* feat: add deposits table to deposits tab on portfolio page * feat: refactor use-withdraw hook to not invoke eth tx * feat: rename hook for clarity * feat: fix withdrawal creation test * feat: update withdraw-manager and withrawals-table tests * chore: fix lint * feat: remove web3 input to avoid double dialog * chore: use renderHook from testing-library/react * chore: update to use non deprecated fields * chore: remove usage of all bridge contract * feat: correctly merge cache update in withdrawals table * feat: changes to support token app withdrawals * chore: add height to ag grid table wrapping element * feat: add txhash col to withdraw table * feat: provide better ui if withdrawal is not ready to be completed * feat: use separate dialogs for txs * feat: allow user to immediately complete withdrawal if delay not triggered * feat: add withdraw store to tidy up state management * chore: fix tests * chore: convert callback to promises, fix tests, delete withdraw page * chore: fix lint errors * fix: withdrawals link in nav * feat: style changes after design update * fix: proposal form test * chore: tidy error ui logic * feat: review comments * chore: lint * feat: add better typing for tables * chore: put withdrawals tab at the end * chore: update i18n * fix: dialog in positions manager due to rename * chore: increase spacing in withdrawal form * chore: update tests * chore: lint * chore: use new assetsConnection and update cy test * fix: incorrect shape of withdrawal generate function * feat: delete withdrawals page now that its shown on the portfolio page * chore: update tests to check for withdrawals page * chore: fix tests again * fix: page title test
This commit is contained in:
parent
578d6ecf6f
commit
5eb06254de
@ -110,8 +110,7 @@ export const DealTicketSteps = ({
|
||||
fieldErrors: errors,
|
||||
});
|
||||
|
||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
||||
useOrderSubmit();
|
||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||
|
||||
const onSizeChange = (value: number[]) => {
|
||||
const newVal = new BigNumber(value[0])
|
||||
@ -249,13 +248,13 @@ export const DealTicketSteps = ({
|
||||
notionalSize={notionalSize || emptyString}
|
||||
fees={fees || emptyString}
|
||||
/>
|
||||
<TransactionDialog
|
||||
<Dialog
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</TransactionDialog>
|
||||
</Dialog>
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
|
@ -3,7 +3,7 @@ const navHome = '[href="/"]';
|
||||
const navVesting = '[href="/vesting"]';
|
||||
const navStaking = '[href="/staking"]';
|
||||
const navRewards = '[href="/rewards"]';
|
||||
const navWithdraw = '[href="/withdraw"]';
|
||||
const navWithdraw = '[href="/withdrawals"]';
|
||||
const navGovernance = '[href="/governance"]';
|
||||
|
||||
const tokenDetailsTable = '.token-details';
|
||||
|
@ -1,18 +1,17 @@
|
||||
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
|
||||
const warning = '[data-testid="callout"]';
|
||||
|
||||
context('Withdraw Page - verify elements on page', function () {
|
||||
before('navigate to withdraw page', function () {
|
||||
cy.visit('/').navigate_to('withdraw');
|
||||
before('navigate to withdrawals page', function () {
|
||||
cy.visit('/').navigate_to('withdrawals');
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', function () {
|
||||
it('should have withdraw tab highlighted', function () {
|
||||
cy.verify_tab_highlighted('withdraw');
|
||||
cy.verify_tab_highlighted('withdrawals');
|
||||
});
|
||||
|
||||
it('should have WITHDRAW header visible', function () {
|
||||
cy.verify_page_header('Withdraw');
|
||||
cy.verify_page_header('Withdrawals');
|
||||
});
|
||||
|
||||
it('should have connect Vega wallet button', function () {
|
||||
@ -20,9 +19,5 @@ context('Withdraw Page - verify elements on page', function () {
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Connect Vega wallet');
|
||||
});
|
||||
|
||||
it('should have withdraw information box', function () {
|
||||
cy.get(warning).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ const navigation = {
|
||||
vesting: '[href="/vesting"]',
|
||||
staking: '[href="/staking"]',
|
||||
rewards: '[href="/rewards"]',
|
||||
withdraw: '[href="/withdraw"]',
|
||||
withdrawals: '[href="/withdrawals"]',
|
||||
governance: '[href="/governance"]',
|
||||
pageSpinner: '[data-testid="splash-loader"]',
|
||||
};
|
||||
|
@ -224,7 +224,7 @@ const NavLinks = ({
|
||||
{ route: Routes.VESTING, text: t('Vesting') },
|
||||
{ route: Routes.STAKING, text: t('Staking') },
|
||||
{ route: Routes.REWARDS, text: t('Rewards') },
|
||||
{ route: Routes.WITHDRAW, text: t('Withdraw') },
|
||||
{ route: Routes.WITHDRAWALS, text: t('Withdraw') },
|
||||
{ route: Routes.GOVERNANCE, text: t('Governance') },
|
||||
];
|
||||
const navClasses = classNames('flex', {
|
||||
|
@ -452,7 +452,7 @@
|
||||
"withdrawFormAmountLabel": "How much would you like to withdraw?",
|
||||
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
|
||||
"withdrawFormSubmitButtonPending": "Preparing",
|
||||
"withdrawalsTitle": "Incomplete withdrawals",
|
||||
"withdrawalsTitle": "Withdrawals",
|
||||
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
|
||||
"withdrawalsNone": "You don't have any pending withdrawals.",
|
||||
"withdrawalsCompleteButton": "Finish withdrawal",
|
||||
|
@ -143,13 +143,6 @@ const LazyContracts = React.lazy(
|
||||
)
|
||||
);
|
||||
|
||||
const LazyWithdraw = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "route-withdraw", webpackPrefetch: true */ './withdraw'
|
||||
)
|
||||
);
|
||||
|
||||
const LazyWithdrawals = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
@ -201,11 +194,6 @@ const routerConfig = [
|
||||
name: 'Rewards',
|
||||
component: LazyRewards,
|
||||
},
|
||||
{
|
||||
path: Routes.WITHDRAW,
|
||||
name: 'Withdraw',
|
||||
component: LazyWithdraw,
|
||||
},
|
||||
{
|
||||
path: Routes.WITHDRAWALS,
|
||||
name: 'Withdrawals',
|
||||
|
@ -4,7 +4,6 @@ export default {
|
||||
CLAIM: '/claim',
|
||||
STAKING: '/staking',
|
||||
REWARDS: '/rewards',
|
||||
WITHDRAW: '/withdraw',
|
||||
WITHDRAWALS: '/withdrawals',
|
||||
GOVERNANCE: '/governance',
|
||||
VESTING: '/vesting',
|
||||
|
@ -1,207 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AccountType, WithdrawalStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: WithdrawPage
|
||||
// ====================================================
|
||||
|
||||
export interface WithdrawPage_party_accounts_asset_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party_accounts_asset_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawPage_party_accounts_asset_source = WithdrawPage_party_accounts_asset_source_BuiltinAsset | WithdrawPage_party_accounts_asset_source_ERC20;
|
||||
|
||||
export interface WithdrawPage_party_accounts_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawPage_party_accounts_asset_source;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party_accounts {
|
||||
__typename: "Account";
|
||||
/**
|
||||
* Balance as string - current account balance (approx. as balances can be updated several times per second)
|
||||
*/
|
||||
balance: string;
|
||||
balanceFormatted: string;
|
||||
/**
|
||||
* Account type (General, Margin, etc)
|
||||
*/
|
||||
type: AccountType;
|
||||
/**
|
||||
* Asset, the 'currency'
|
||||
*/
|
||||
asset: WithdrawPage_party_accounts_asset;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party_withdrawals_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party_withdrawals_details {
|
||||
__typename: "Erc20WithdrawalDetails";
|
||||
/**
|
||||
* The ethereum address of the receiver of the asset funds
|
||||
*/
|
||||
receiverAddress: string;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party_withdrawals {
|
||||
__typename: "Withdrawal";
|
||||
/**
|
||||
* The Vega internal ID of the withdrawal
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The amount to be withdrawn
|
||||
*/
|
||||
amount: string;
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: WithdrawPage_party_withdrawals_asset;
|
||||
/**
|
||||
* The current status of the withdrawal
|
||||
*/
|
||||
status: WithdrawalStatus;
|
||||
/**
|
||||
* RFC3339Nano time at which the withdrawal was created
|
||||
*/
|
||||
createdTimestamp: string;
|
||||
/**
|
||||
* RFC3339Nano time at which the withdrawal was finalised
|
||||
*/
|
||||
withdrawnTimestamp: string | null;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
/**
|
||||
* Foreign chain specific details about the withdrawal
|
||||
*/
|
||||
details: WithdrawPage_party_withdrawals_details | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Collateral accounts relating to a party
|
||||
*/
|
||||
accounts: WithdrawPage_party_accounts[] | null;
|
||||
/**
|
||||
* The list of all withdrawals initiated by the party
|
||||
*/
|
||||
withdrawals: WithdrawPage_party_withdrawals[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_assetsConnection_edges_node_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawPage_assetsConnection_edges_node_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawPage_assetsConnection_edges_node_source = WithdrawPage_assetsConnection_edges_node_source_BuiltinAsset | WithdrawPage_assetsConnection_edges_node_source_ERC20;
|
||||
|
||||
export interface WithdrawPage_assetsConnection_edges_node {
|
||||
__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. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawPage_assetsConnection_edges_node_source;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_assetsConnection_edges {
|
||||
__typename: "AssetEdge";
|
||||
node: WithdrawPage_assetsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface WithdrawPage_assetsConnection {
|
||||
__typename: "AssetsConnection";
|
||||
/**
|
||||
* The assets
|
||||
*/
|
||||
edges: (WithdrawPage_assetsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPage {
|
||||
/**
|
||||
* An entity that is trading on the Vega network
|
||||
*/
|
||||
party: WithdrawPage_party | null;
|
||||
/**
|
||||
* The list of all assets in use in the Vega network or the specified asset if ID is provided
|
||||
*/
|
||||
assetsConnection: WithdrawPage_assetsConnection;
|
||||
}
|
||||
|
||||
export interface WithdrawPageVariables {
|
||||
partyId: string;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import {
|
||||
Callout,
|
||||
FormGroup,
|
||||
Intent,
|
||||
Input,
|
||||
ButtonLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface EthAddressSelectorProps {
|
||||
address: string;
|
||||
connectedAddress: string;
|
||||
onChange: (newAddress: string) => void;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export const EthAddressInput = ({
|
||||
connectedAddress,
|
||||
address,
|
||||
onChange,
|
||||
isValid,
|
||||
}: EthAddressSelectorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [useConnectedWallet, setUseConnectedWallet] =
|
||||
React.useState<boolean>(true);
|
||||
React.useEffect(() => {
|
||||
if (useConnectedWallet) {
|
||||
onChange(connectedAddress);
|
||||
}
|
||||
}, [connectedAddress, onChange, useConnectedWallet]);
|
||||
|
||||
return (
|
||||
<FormGroup label={t('To')} labelFor="ethAddressInput">
|
||||
<Input
|
||||
data-testid="token-amount-input"
|
||||
className="token-input__input"
|
||||
name="ethAddressInput"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={address}
|
||||
disabled={useConnectedWallet}
|
||||
// leftElement={<Ethereum />} TODO: render Ethereum icon in input when https://github.com/vegaprotocol/frontend-monorepo/issues/273
|
||||
autoComplete="off"
|
||||
type="text"
|
||||
required={true}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<ButtonLink onClick={() => setUseConnectedWallet(!useConnectedWallet)}>
|
||||
{useConnectedWallet ? t('enterAddress') : t('useConnectedWallet')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
{isValid ? null : (
|
||||
<Callout intent={Intent.Warning}>{t('invalidAddress')}</Callout>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
@ -1,166 +0,0 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Heading } from '../../components/heading';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
|
||||
import Routes from '../routes';
|
||||
import type {
|
||||
WithdrawPage,
|
||||
WithdrawPageVariables,
|
||||
} from './__generated__/WithdrawPage';
|
||||
import { WithdrawManager } from '@vegaprotocol/withdraws';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { assetsConnectionToAssets } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const Withdraw = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('withdrawPageHeading')} />
|
||||
<p>{t('withdrawPageText')}</p>
|
||||
<div className="mb-8">
|
||||
<VegaWalletContainer>
|
||||
{(currVegaKey) => <WithdrawContainer currVegaKey={currVegaKey} />}
|
||||
</VegaWalletContainer>
|
||||
</div>
|
||||
<Callout title={t('withdrawPageInfoCalloutTitle')}>
|
||||
<p className="mb-0">{t('withdrawPageInfoCalloutText')}</p>
|
||||
</Callout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WITHDRAW_PAGE_QUERY = gql`
|
||||
query WithdrawPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
accounts {
|
||||
balance
|
||||
balanceFormatted @client
|
||||
type
|
||||
asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
__typename
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
withdrawals {
|
||||
id
|
||||
amount
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
status
|
||||
createdTimestamp
|
||||
withdrawnTimestamp
|
||||
txHash
|
||||
details {
|
||||
... on Erc20WithdrawalDetails {
|
||||
receiverAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface WithdrawContainerProps {
|
||||
currVegaKey: VegaKeyExtended;
|
||||
}
|
||||
|
||||
export const WithdrawContainer = ({ currVegaKey }: WithdrawContainerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useQuery<
|
||||
WithdrawPage,
|
||||
WithdrawPageVariables
|
||||
>(WITHDRAW_PAGE_QUERY, {
|
||||
variables: { partyId: currVegaKey?.pub },
|
||||
});
|
||||
|
||||
const accounts = React.useMemo(() => {
|
||||
if (!data?.party?.accounts) return [];
|
||||
// You can only withdraw from general accounts
|
||||
return data.party.accounts.filter(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
// Note there is a small period where the withdrawal might have a tx hash but is technically
|
||||
// not complete yet as the tx hash gets set before the transaction is confirmed
|
||||
const hasPendingWithdrawals = React.useMemo(() => {
|
||||
if (!data?.party?.withdrawals?.length) return false;
|
||||
return data.party.withdrawals.some((w) => w.txHash === null);
|
||||
}, [data]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section>
|
||||
<p>{t('Something went wrong')}</p>
|
||||
{error && <pre>{error.message}</pre>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
const assets = assetsConnectionToAssets(data.assetsConnection);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasPendingWithdrawals && (
|
||||
<div className="mb-8">
|
||||
<Callout
|
||||
title={t('pendingWithdrawalsCalloutTitle')}
|
||||
intent={Intent.Warning}
|
||||
>
|
||||
<p>{t('pendingWithdrawalsCalloutText')}</p>
|
||||
<p>
|
||||
<Link to={Routes.WITHDRAWALS} className="underline text-white">
|
||||
{t('pendingWithdrawalsCalloutButton')}
|
||||
</Link>
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
)}
|
||||
<WithdrawManager assets={assets || []} accounts={accounts} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdraw;
|
@ -1,22 +1,15 @@
|
||||
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { format } from 'date-fns';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import { SplashLoader } from '../../components/splash-loader';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
|
||||
import { addDecimal } from '../../lib/decimals';
|
||||
import { truncateMiddle } from '../../lib/truncate-middle';
|
||||
import type { Withdrawals_party_withdrawals } from '@vegaprotocol/withdraws';
|
||||
import { useCompleteWithdraw, useWithdrawals } from '@vegaprotocol/withdraws';
|
||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||
import {
|
||||
useWithdrawals,
|
||||
WithdrawalDialogs,
|
||||
WithdrawalsTable,
|
||||
} from '@vegaprotocol/withdraws';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Withdrawals = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -32,19 +25,9 @@ const Withdrawals = () => {
|
||||
};
|
||||
|
||||
const WithdrawPendingContainer = () => {
|
||||
const [withdrawDialog, setWithdrawDialog] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { submit, Dialog } = useCompleteWithdraw();
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
|
||||
const withdrawals = React.useMemo(() => {
|
||||
if (!data?.party?.withdrawals?.length) return [];
|
||||
|
||||
return orderBy(
|
||||
data.party.withdrawals,
|
||||
[(w) => new Date(w.createdTimestamp)],
|
||||
['desc']
|
||||
);
|
||||
}, [data]);
|
||||
const { withdrawals, loading, error } = useWithdrawals();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@ -55,7 +38,7 @@ const WithdrawPendingContainer = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !data) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
@ -63,122 +46,23 @@ const WithdrawPendingContainer = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!withdrawals.length) {
|
||||
return <p>{t('withdrawalsNone')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('withdrawalsPreparedWarningHeading')}</h2>
|
||||
<header className="flex items-start justify-between">
|
||||
<h2>{t('withdrawalsPreparedWarningHeading')}</h2>
|
||||
<Button onClick={() => setWithdrawDialog(true)}>Withdraw</Button>
|
||||
</header>
|
||||
<p>{t('withdrawalsText')}</p>
|
||||
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
|
||||
<ul role="list">
|
||||
{withdrawals.map((w) => (
|
||||
<li key={w.id} className="mb-10 last:mb-0">
|
||||
<Withdrawal withdrawal={w} complete={submit} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Dialog />
|
||||
<div className="w-full h-[500px]">
|
||||
<WithdrawalsTable withdrawals={withdrawals} />
|
||||
</div>
|
||||
<WithdrawalDialogs
|
||||
withdrawDialog={withdrawDialog}
|
||||
setWithdrawDialog={setWithdrawDialog}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface WithdrawalProps {
|
||||
withdrawal: Withdrawals_party_withdrawals;
|
||||
complete: (withdrawalId: string) => void;
|
||||
}
|
||||
|
||||
export const Withdrawal = ({ withdrawal, complete }: WithdrawalProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
let status;
|
||||
let footer = null;
|
||||
|
||||
if (withdrawal.pendingOnForeignChain) {
|
||||
status = t('Pending');
|
||||
footer = (
|
||||
<Button
|
||||
fill={true}
|
||||
disabled={true}
|
||||
onClick={() => complete(withdrawal.id)}
|
||||
>
|
||||
{t('withdrawalsCompleteButton')}
|
||||
</Button>
|
||||
);
|
||||
} else if (withdrawal.status === WithdrawalStatus.STATUS_FINALIZED) {
|
||||
if (withdrawal.txHash) {
|
||||
status = t('Complete');
|
||||
} else {
|
||||
status = t('Incomplete');
|
||||
footer = (
|
||||
<Button fill={true} onClick={() => complete(withdrawal.id)}>
|
||||
{t('withdrawalsCompleteButton')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
status = withdrawal.status;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('Withdraw')}
|
||||
<span>
|
||||
{addDecimal(
|
||||
new BigNumber(withdrawal.amount),
|
||||
withdrawal.asset.decimals
|
||||
)}{' '}
|
||||
{withdrawal.asset.symbol}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('toEthereum')}
|
||||
<span>
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/tx/${withdrawal.details?.receiverAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{truncateMiddle(withdrawal.details?.receiverAddress ?? '')}
|
||||
</Link>
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('created')}
|
||||
<span>
|
||||
{format(
|
||||
new Date(withdrawal.createdTimestamp),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('withdrawalTransaction', { foreignChain: 'Ethereum' })}
|
||||
<span>
|
||||
{withdrawal.txHash ? (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${withdrawal.txHash}`}
|
||||
target="_blank"
|
||||
>
|
||||
{truncateMiddle(withdrawal.txHash)}
|
||||
</Link>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('status')}
|
||||
{status}
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdrawals;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||
import { generateWithdrawPageQuery } from '../support/mocks/generate-withdraw-page-query';
|
||||
import { generateWithdrawFormQuery } from '../support/mocks/generate-withdraw-page-query';
|
||||
import { generateWithdrawals } from '../support/mocks/generate-withdrawals';
|
||||
import { connectVegaWallet } from '../support/vega-wallet';
|
||||
|
||||
describe('withdraw', () => {
|
||||
@ -16,10 +17,13 @@ describe('withdraw', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockGQL((req) => {
|
||||
aliasQuery(req, 'WithdrawPageQuery', generateWithdrawPageQuery());
|
||||
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||
aliasQuery(req, 'WithdrawFormQuery', generateWithdrawFormQuery());
|
||||
});
|
||||
cy.visit('/portfolio/withdraw');
|
||||
|
||||
cy.visit('/portfolio');
|
||||
cy.getByTestId('Withdrawals').click();
|
||||
|
||||
// Withdraw page requires vega wallet connection
|
||||
connectVegaWallet();
|
||||
@ -27,15 +31,14 @@ describe('withdraw', () => {
|
||||
// It also requires connection Ethereum wallet
|
||||
connectEthereumWallet();
|
||||
|
||||
cy.wait('@WithdrawPageQuery');
|
||||
cy.contains('Withdraw');
|
||||
cy.mockGQL((req) => {
|
||||
aliasQuery(req, 'WithdrawFormQuery', generateWithdrawFormQuery());
|
||||
});
|
||||
cy.getByTestId('withdraw-dialog-button').click();
|
||||
cy.wait('@WithdrawFormQuery');
|
||||
});
|
||||
|
||||
it('form validation', () => {
|
||||
// Prompts that there are incomplete withdrawals
|
||||
cy.contains('You have incomplete withdrawals');
|
||||
cy.getByTestId('complete-withdrawals-prompt').should('exist');
|
||||
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
@ -82,9 +85,8 @@ describe('withdraw', () => {
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.getByTestId('dialog-title').should(
|
||||
'have.text',
|
||||
'Withdrawal transaction pending'
|
||||
'Awaiting network confirmation'
|
||||
);
|
||||
cy.getByTestId('dialog-text').should('have.text', 'Awaiting transaction');
|
||||
});
|
||||
|
||||
it.skip('creates a withdrawal on submit'); // Needs capsule
|
||||
|
@ -1,77 +0,0 @@
|
||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
||||
import { generateWithdrawals } from '../support/mocks/generate-withdrawals';
|
||||
import { connectVegaWallet } from '../support/vega-wallet';
|
||||
|
||||
describe('withdrawals', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockGQL((req) => {
|
||||
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||
});
|
||||
cy.visit('/portfolio/withdrawals');
|
||||
|
||||
// Withdraw page requires vega wallet connection
|
||||
connectVegaWallet();
|
||||
|
||||
// It also requires connection Ethereum wallet
|
||||
connectEthereumWallet();
|
||||
});
|
||||
|
||||
it('renders history of withdrawals', () => {
|
||||
const ethAddressLink = `${Cypress.env(
|
||||
'ETHERSCAN_URL'
|
||||
)}/address/0x72c22822A19D20DE7e426fB84aa047399Ddd8853`;
|
||||
const etherScanLink = `${Cypress.env(
|
||||
'ETHERSCAN_URL'
|
||||
)}/tx/0x5d7b1a35ba6bd23be17bb7a159c13cdbb3121fceb94e9c6c510f5503dce48d03`;
|
||||
|
||||
const row = '.ag-center-cols-container[role="rowgroup"] > [role="row"]';
|
||||
|
||||
// First row is incomplete
|
||||
cy.get(row)
|
||||
.eq(0)
|
||||
.find('[col-id="asset.symbol"]')
|
||||
.should('contain.text', 'AST0');
|
||||
cy.get(row)
|
||||
.eq(0)
|
||||
.find('[col-id="amount"]')
|
||||
.should('contain.text', '0.00100');
|
||||
cy.get(row)
|
||||
.eq(0)
|
||||
.find('[col-id="details.receiverAddress"]')
|
||||
.should('contain.text', '0x72c2…dd8853')
|
||||
.find('a')
|
||||
.should('have.attr', 'href', ethAddressLink);
|
||||
cy.get(row)
|
||||
.eq(0)
|
||||
.find('[col-id="createdTimestamp"]')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.get(row)
|
||||
.eq(0)
|
||||
.find('[col-id="status"]')
|
||||
.should('contain.text', 'Open')
|
||||
.find('button')
|
||||
.contains('Click to complete');
|
||||
|
||||
// Second row is complete so last cell should have a link to the tx
|
||||
cy.get(row)
|
||||
.eq(1)
|
||||
.find('[col-id="status"]')
|
||||
.should('contain.text', 'Finalized')
|
||||
.find('a')
|
||||
.contains('View on Etherscan')
|
||||
.should('have.attr', 'href', etherScanLink);
|
||||
});
|
||||
|
||||
it('renders a link to start a new withdrawal', () => {
|
||||
cy.getByTestId('start-withdrawal').click();
|
||||
cy.url().should('include', '/portfolio/withdraw');
|
||||
});
|
||||
|
||||
it.skip('renders pending and unfinished withdrawals');
|
||||
it.skip('can complete unfinished withdrawals'); // Needs capsule
|
||||
});
|
@ -2,7 +2,7 @@ import { AccountType } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
export const generateWithdrawPageQuery = (
|
||||
export const generateWithdrawFormQuery = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override?: PartialDeep<any>
|
||||
) => {
|
||||
|
@ -1,55 +1,74 @@
|
||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||
import type { Withdrawals } from '@vegaprotocol/withdraws';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
export const generateWithdrawals = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override?: PartialDeep<any>
|
||||
) => {
|
||||
const defaultResult = {
|
||||
export const generateWithdrawals = (override?: PartialDeep<Withdrawals>) => {
|
||||
const defaultResult: Withdrawals = {
|
||||
party: {
|
||||
id: 'party-0',
|
||||
withdrawals: [
|
||||
{
|
||||
id: 'withdrawal-0',
|
||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||
amount: '100',
|
||||
txHash: null,
|
||||
createdTimestamp: new Date('2022-02-02').toISOString(),
|
||||
withdrawnTimestamp: new Date('2022-02-02').toISOString(),
|
||||
details: {
|
||||
__typename: 'Erc20WithdrawalDetails',
|
||||
receiverAddress: '0x72c22822A19D20DE7e426fB84aa047399Ddd8853',
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: {
|
||||
id: 'withdrawal-0',
|
||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||
amount: '100',
|
||||
txHash: null,
|
||||
createdTimestamp: new Date('2022-02-02').toISOString(),
|
||||
withdrawnTimestamp: new Date('2022-02-02').toISOString(),
|
||||
pendingOnForeignChain: false,
|
||||
details: {
|
||||
__typename: 'Erc20WithdrawalDetails',
|
||||
receiverAddress: '0x72c22822A19D20DE7e426fB84aa047399Ddd8853',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
name: 'asset-0 name',
|
||||
symbol: 'AST0',
|
||||
decimals: 5,
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x123',
|
||||
},
|
||||
},
|
||||
__typename: 'Withdrawal',
|
||||
},
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
symbol: 'AST0',
|
||||
decimals: 5,
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: {
|
||||
id: 'withdrawal-1',
|
||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||
amount: '100',
|
||||
txHash:
|
||||
'0x5d7b1a35ba6bd23be17bb7a159c13cdbb3121fceb94e9c6c510f5503dce48d03',
|
||||
createdTimestamp: new Date('2022-02-01').toISOString(),
|
||||
withdrawnTimestamp: new Date('2022-02-01').toISOString(),
|
||||
pendingOnForeignChain: false,
|
||||
details: {
|
||||
__typename: 'Erc20WithdrawalDetails',
|
||||
receiverAddress: '0x72c22822A19D20DE7e426fB84aa047399Ddd8853',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
name: 'asset-0 name',
|
||||
symbol: 'AST0',
|
||||
decimals: 5,
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x123',
|
||||
},
|
||||
},
|
||||
__typename: 'Withdrawal',
|
||||
},
|
||||
},
|
||||
__typename: 'Withdrawal',
|
||||
},
|
||||
{
|
||||
id: 'withdrawal-1',
|
||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||
amount: '100',
|
||||
txHash:
|
||||
'0x5d7b1a35ba6bd23be17bb7a159c13cdbb3121fceb94e9c6c510f5503dce48d03',
|
||||
createdTimestamp: new Date('2022-02-01').toISOString(),
|
||||
withdrawnTimestamp: new Date('2022-02-01').toISOString(),
|
||||
details: {
|
||||
__typename: 'Erc20WithdrawalDetails',
|
||||
receiverAddress: '0x72c22822A19D20DE7e426fB84aa047399Ddd8853',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-0',
|
||||
symbol: 'AST0',
|
||||
decimals: 5,
|
||||
},
|
||||
__typename: 'Withdrawal',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
|
||||
NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql
|
||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
|
||||
NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql
|
||||
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||
|
@ -51,6 +51,9 @@ export function createClient(base?: string) {
|
||||
},
|
||||
},
|
||||
},
|
||||
ERC20: {
|
||||
keyFields: ['contractAddress'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
46
apps/trading/pages/portfolio/__generated__/AssetFields.ts
generated
Normal file
46
apps/trading/pages/portfolio/__generated__/AssetFields.ts
generated
Normal file
@ -0,0 +1,46 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: AssetFields
|
||||
// ====================================================
|
||||
|
||||
export interface AssetFields_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface AssetFields_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type AssetFields_source = AssetFields_source_BuiltinAsset | AssetFields_source_ERC20;
|
||||
|
||||
export interface AssetFields {
|
||||
__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. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: AssetFields_source;
|
||||
}
|
119
apps/trading/pages/portfolio/__generated__/WithdrawFormQuery.ts
generated
Normal file
119
apps/trading/pages/portfolio/__generated__/WithdrawFormQuery.ts
generated
Normal file
@ -0,0 +1,119 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AccountType } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: WithdrawFormQuery
|
||||
// ====================================================
|
||||
|
||||
export interface WithdrawFormQuery_party_withdrawals {
|
||||
__typename: "Withdrawal";
|
||||
/**
|
||||
* The Vega internal ID of the withdrawal
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery_party_accounts_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery_party_accounts {
|
||||
__typename: "Account";
|
||||
/**
|
||||
* Account type (General, Margin, etc)
|
||||
*/
|
||||
type: AccountType;
|
||||
/**
|
||||
* Balance as string - current account balance (approx. as balances can be updated several times per second)
|
||||
*/
|
||||
balance: string;
|
||||
/**
|
||||
* Asset, the 'currency'
|
||||
*/
|
||||
asset: WithdrawFormQuery_party_accounts_asset;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The list of all withdrawals initiated by the party
|
||||
*/
|
||||
withdrawals: WithdrawFormQuery_party_withdrawals[] | null;
|
||||
/**
|
||||
* Collateral accounts relating to a party
|
||||
*/
|
||||
accounts: WithdrawFormQuery_party_accounts[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery_assets_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery_assets_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawFormQuery_assets_source = WithdrawFormQuery_assets_source_BuiltinAsset | WithdrawFormQuery_assets_source_ERC20;
|
||||
|
||||
export interface WithdrawFormQuery_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. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawFormQuery_assets_source;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQuery {
|
||||
/**
|
||||
* An entity that is trading on the Vega network
|
||||
*/
|
||||
party: WithdrawFormQuery_party | null;
|
||||
/**
|
||||
* The list of all assets in use in the Vega network
|
||||
*/
|
||||
assets: WithdrawFormQuery_assets[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawFormQueryVariables {
|
||||
partyId: string;
|
||||
}
|
@ -1,17 +1,29 @@
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { DepositsTable } from '@vegaprotocol/deposits';
|
||||
import { useDeposits } from '@vegaprotocol/deposits';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const DepositsContainer = () => {
|
||||
const { deposits, loading, error } = useDeposits();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_min-content] gap-4 h-full">
|
||||
<div />
|
||||
<div className="p-4">
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<header className="flex justify-between items-center p-4">
|
||||
<h4 className="text-lg text-black dark:text-white">{t('Deposits')}</h4>
|
||||
<Link href="/portfolio/deposit" passHref={true}>
|
||||
<Button size="md" data-testid="deposit">
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
<Button>Deposit</Button>
|
||||
</Link>
|
||||
</header>
|
||||
<div>
|
||||
<AsyncRenderer
|
||||
data={deposits}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={(data) => {
|
||||
return <DepositsTable deposits={data} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||
@ -67,14 +66,12 @@ const Portfolio = () => {
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="deposits" name={t('Deposits')}>
|
||||
<DepositsContainer />
|
||||
<VegaWalletContainer>
|
||||
<DepositsContainer />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="withdrawals" name={t('Withdrawals')}>
|
||||
<Web3Container>
|
||||
<VegaWalletContainer>
|
||||
<WithdrawalsContainer />
|
||||
</VegaWalletContainer>
|
||||
</Web3Container>
|
||||
<WithdrawalsContainer />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</PortfolioGridChild>
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
import { WithdrawPageContainer } from './withdraw-page-container';
|
||||
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const Withdraw = () => {
|
||||
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 (
|
||||
<VegaWalletContainer>
|
||||
<Web3Container>
|
||||
<div className="max-w-[420px] p-8 mx-auto">
|
||||
<h1 className="text-2xl mb-4">{t('Withdraw')}</h1>
|
||||
<WithdrawPageContainer assetId={assetId} />
|
||||
</div>
|
||||
</Web3Container>
|
||||
</VegaWalletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdraw;
|
@ -1,101 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { assetsConnectionToAssets, t } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { WithdrawManager } from '@vegaprotocol/withdraws';
|
||||
import { ASSET_FRAGMENT } from '../../../lib/query-fragments';
|
||||
import Link from 'next/link';
|
||||
import { PageQueryContainer } from '../../../components/page-query-container';
|
||||
import type {
|
||||
WithdrawPageQuery,
|
||||
WithdrawPageQueryVariables,
|
||||
} from './__generated__/WithdrawPageQuery';
|
||||
|
||||
const WITHDRAW_PAGE_QUERY = gql`
|
||||
${ASSET_FRAGMENT}
|
||||
query WithdrawPageQuery($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
withdrawals {
|
||||
id
|
||||
txHash
|
||||
}
|
||||
accounts {
|
||||
type
|
||||
balance
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface WithdrawPageContainerProps {
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const WithdrawPageContainer = ({
|
||||
assetId,
|
||||
}: WithdrawPageContainerProps) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<PageQueryContainer<WithdrawPageQuery, WithdrawPageQueryVariables>
|
||||
query={WITHDRAW_PAGE_QUERY}
|
||||
options={{
|
||||
variables: { partyId: keypair?.pub || '' },
|
||||
skip: !keypair?.pub,
|
||||
}}
|
||||
render={(data) => {
|
||||
const assets = assetsConnectionToAssets(data.assetsConnection);
|
||||
if (!assets.length) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('No assets on this network')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
const hasIncompleteWithdrawals = data.party?.withdrawals?.some(
|
||||
(w) => w.txHash === null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasIncompleteWithdrawals ? (
|
||||
<p className="mb-6">
|
||||
{t('You have incomplete withdrawals.')}{' '}
|
||||
<Link href="/portfolio/withdrawals" passHref={true}>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
className="underline"
|
||||
data-testid="complete-withdrawals-prompt"
|
||||
>
|
||||
{t('Click here to finish withdrawal')}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
) : null}
|
||||
<WithdrawManager
|
||||
assets={assets}
|
||||
accounts={data.party?.accounts || []}
|
||||
initialAssetId={assetId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,36 +1,50 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useWithdrawals, WithdrawalsTable } from '@vegaprotocol/withdraws';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
useWithdrawals,
|
||||
WithdrawalDialogs,
|
||||
WithdrawalsTable,
|
||||
} from '@vegaprotocol/withdraws';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useState } from 'react';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
|
||||
export const WithdrawalsContainer = () => {
|
||||
const { data, loading, error } = useWithdrawals();
|
||||
const { withdrawals, loading, error } = useWithdrawals();
|
||||
const [withdrawDialog, setWithdrawDialog] = useState(false);
|
||||
|
||||
console.log('render');
|
||||
return (
|
||||
<AsyncRenderer
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={(data) => {
|
||||
const withdrawals = orderBy(
|
||||
data.party?.withdrawals || [],
|
||||
(w) => new Date(w.createdTimestamp).getTime(),
|
||||
'desc'
|
||||
);
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_min-content] gap-4 h-full">
|
||||
<WithdrawalsTable withdrawals={withdrawals} />
|
||||
<div className="p-4">
|
||||
<Link href="/portfolio/withdraw" passHref={true}>
|
||||
<Button size="md" data-testid="start-withdrawal">
|
||||
{t('Withdraw')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Web3Container>
|
||||
<VegaWalletContainer>
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<header className="flex justify-between items-center p-4">
|
||||
<h4 className="text-lg text-black dark:text-white">
|
||||
{t('Withdrawals')}
|
||||
</h4>
|
||||
<Button
|
||||
onClick={() => setWithdrawDialog(true)}
|
||||
data-testid="withdraw-dialog-button"
|
||||
>
|
||||
{t('Withdraw')}
|
||||
</Button>
|
||||
</header>
|
||||
<div>
|
||||
<AsyncRenderer
|
||||
data={withdrawals}
|
||||
loading={loading}
|
||||
error={error}
|
||||
render={(data) => {
|
||||
return <WithdrawalsTable withdrawals={data} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<WithdrawalDialogs
|
||||
withdrawDialog={withdrawDialog}
|
||||
setWithdrawDialog={setWithdrawDialog}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Web3Container>
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { VegaWalletContainer } from '../../../components/vega-wallet-container';
|
||||
import { Web3Container } from '@vegaprotocol/web3';
|
||||
import { WithdrawalsContainer } from '../withdrawals-container';
|
||||
|
||||
const Withdrawals = () => {
|
||||
return (
|
||||
<VegaWalletContainer>
|
||||
<Web3Container>
|
||||
<WithdrawalsContainer />
|
||||
</Web3Container>
|
||||
</VegaWalletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdrawals;
|
@ -16,8 +16,7 @@ export const DealTicketManager = ({
|
||||
market,
|
||||
children,
|
||||
}: DealTicketManagerProps) => {
|
||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
||||
useOrderSubmit();
|
||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -33,13 +32,13 @@ export const DealTicketManager = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TransactionDialog
|
||||
<Dialog
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
</TransactionDialog>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1 +1,3 @@
|
||||
export * from './lib/deposit-manager';
|
||||
export * from './lib/use-deposits';
|
||||
export * from './lib/deposits-table';
|
||||
|
83
libs/deposits/src/lib/__generated__/DepositEventSub.ts
generated
Normal file
83
libs/deposits/src/lib/__generated__/DepositEventSub.ts
generated
Normal file
@ -0,0 +1,83 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { DepositStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: DepositEventSub
|
||||
// ====================================================
|
||||
|
||||
export interface DepositEventSub_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 DepositEventSub_busEvents_event_Deposit_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface DepositEventSub_busEvents_event_Deposit {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal ID of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
/**
|
||||
* The amount to be withdrawn
|
||||
*/
|
||||
amount: string;
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: DepositEventSub_busEvents_event_Deposit_asset;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was created
|
||||
*/
|
||||
createdTimestamp: string;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was finalised
|
||||
*/
|
||||
creditedTimestamp: string | null;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
}
|
||||
|
||||
export type DepositEventSub_busEvents_event = DepositEventSub_busEvents_event_TimeUpdate | DepositEventSub_busEvents_event_Deposit;
|
||||
|
||||
export interface DepositEventSub_busEvents {
|
||||
__typename: "BusEvent";
|
||||
/**
|
||||
* the payload - the wrapped event
|
||||
*/
|
||||
event: DepositEventSub_busEvents_event;
|
||||
}
|
||||
|
||||
export interface DepositEventSub {
|
||||
/**
|
||||
* Subscribe to event data from the event bus
|
||||
*/
|
||||
busEvents: DepositEventSub_busEvents[] | null;
|
||||
}
|
||||
|
||||
export interface DepositEventSubVariables {
|
||||
partyId: string;
|
||||
}
|
58
libs/deposits/src/lib/__generated__/DepositFields.ts
generated
Normal file
58
libs/deposits/src/lib/__generated__/DepositFields.ts
generated
Normal file
@ -0,0 +1,58 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { DepositStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: DepositFields
|
||||
// ====================================================
|
||||
|
||||
export interface DepositFields_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface DepositFields {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal ID of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
/**
|
||||
* The amount to be withdrawn
|
||||
*/
|
||||
amount: string;
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: DepositFields_asset;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was created
|
||||
*/
|
||||
createdTimestamp: string;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was finalised
|
||||
*/
|
||||
creditedTimestamp: string | null;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
}
|
94
libs/deposits/src/lib/__generated__/Deposits.ts
generated
Normal file
94
libs/deposits/src/lib/__generated__/Deposits.ts
generated
Normal file
@ -0,0 +1,94 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { DepositStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: Deposits
|
||||
// ====================================================
|
||||
|
||||
export interface Deposits_party_depositsConnection_edges_node_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface Deposits_party_depositsConnection_edges_node {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal ID of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
/**
|
||||
* The amount to be withdrawn
|
||||
*/
|
||||
amount: string;
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: Deposits_party_depositsConnection_edges_node_asset;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was created
|
||||
*/
|
||||
createdTimestamp: string;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was finalised
|
||||
*/
|
||||
creditedTimestamp: string | null;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
}
|
||||
|
||||
export interface Deposits_party_depositsConnection_edges {
|
||||
__typename: "DepositEdge";
|
||||
node: Deposits_party_depositsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface Deposits_party_depositsConnection {
|
||||
__typename: "DepositsConnection";
|
||||
/**
|
||||
* The deposits
|
||||
*/
|
||||
edges: (Deposits_party_depositsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Deposits_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The list of all deposits for a party by the party
|
||||
*/
|
||||
depositsConnection: Deposits_party_depositsConnection;
|
||||
}
|
||||
|
||||
export interface Deposits {
|
||||
/**
|
||||
* An entity that is trading on the Vega network
|
||||
*/
|
||||
party: Deposits_party | null;
|
||||
}
|
||||
|
||||
export interface DepositsVariables {
|
||||
partyId: string;
|
||||
}
|
94
libs/deposits/src/lib/__generated__/DepositsQuery.ts
generated
Normal file
94
libs/deposits/src/lib/__generated__/DepositsQuery.ts
generated
Normal file
@ -0,0 +1,94 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { DepositStatus } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: DepositsQuery
|
||||
// ====================================================
|
||||
|
||||
export interface DepositsQuery_party_depositsConnection_edges_node_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface DepositsQuery_party_depositsConnection_edges_node {
|
||||
__typename: "Deposit";
|
||||
/**
|
||||
* The Vega internal ID of the deposit
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The current status of the deposit
|
||||
*/
|
||||
status: DepositStatus;
|
||||
/**
|
||||
* The amount to be withdrawn
|
||||
*/
|
||||
amount: string;
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: DepositsQuery_party_depositsConnection_edges_node_asset;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was created
|
||||
*/
|
||||
createdTimestamp: string;
|
||||
/**
|
||||
* RFC3339Nano time at which the deposit was finalised
|
||||
*/
|
||||
creditedTimestamp: string | null;
|
||||
/**
|
||||
* Hash of the transaction on the foreign chain
|
||||
*/
|
||||
txHash: string | null;
|
||||
}
|
||||
|
||||
export interface DepositsQuery_party_depositsConnection_edges {
|
||||
__typename: "DepositEdge";
|
||||
node: DepositsQuery_party_depositsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface DepositsQuery_party_depositsConnection {
|
||||
__typename: "DepositsConnection";
|
||||
/**
|
||||
* The deposits
|
||||
*/
|
||||
edges: (DepositsQuery_party_depositsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface DepositsQuery_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The list of all deposits for a party by the party
|
||||
*/
|
||||
depositsConnection: DepositsQuery_party_depositsConnection;
|
||||
}
|
||||
|
||||
export interface DepositsQuery {
|
||||
/**
|
||||
* An entity that is trading on the Vega network
|
||||
*/
|
||||
party: DepositsQuery_party | null;
|
||||
}
|
||||
|
||||
export interface DepositsQueryVariables {
|
||||
partyId: string;
|
||||
}
|
81
libs/deposits/src/lib/deposits-table.tsx
Normal file
81
libs/deposits/src/lib/deposits-table.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import {
|
||||
t,
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
truncateByChars,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import type { DepositFields } from './__generated__/DepositFields';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DepositStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
export interface DepositsTableProps {
|
||||
deposits: DepositFields[];
|
||||
}
|
||||
|
||||
export const DepositsTable = ({ deposits }: DepositsTableProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
return (
|
||||
<AgGrid
|
||||
rowData={deposits}
|
||||
overlayNoRowsTemplate={t('No deposits')}
|
||||
defaultColDef={{ flex: 1, resizable: true }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
suppressCellFocus={true}
|
||||
>
|
||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||
<AgGridColumn
|
||||
headerName="Amount"
|
||||
field="amount"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<DepositFields, 'amount'>) => {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Created at"
|
||||
field="createdTimestamp"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<DepositFields, 'createdTimestamp'>) => {
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Status"
|
||||
field="status"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<DepositFields, 'status'>) => {
|
||||
return DepositStatusMapping[value];
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Tx hash"
|
||||
field="txHash"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<DepositFields, 'txHash'>) => {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
153
libs/deposits/src/lib/use-deposits.ts
Normal file
153
libs/deposits/src/lib/use-deposits.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import compact from 'lodash/compact';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type {
|
||||
DepositEventSub,
|
||||
DepositEventSubVariables,
|
||||
DepositEventSub_busEvents_event,
|
||||
DepositEventSub_busEvents_event_Deposit,
|
||||
} from './__generated__/DepositEventSub';
|
||||
import type { Deposits, DepositsVariables } from './__generated__/Deposits';
|
||||
|
||||
const DEPOSIT_FRAGMENT = gql`
|
||||
fragment DepositFields on Deposit {
|
||||
id
|
||||
status
|
||||
amount
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
createdTimestamp
|
||||
creditedTimestamp
|
||||
txHash
|
||||
}
|
||||
`;
|
||||
|
||||
const DEPOSITS_QUERY = gql`
|
||||
${DEPOSIT_FRAGMENT}
|
||||
query DepositsQuery($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
depositsConnection {
|
||||
edges {
|
||||
node {
|
||||
...DepositFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DEPOSITS_BUS_EVENT_SUB = gql`
|
||||
${DEPOSIT_FRAGMENT}
|
||||
subscription DepositEventSub($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Deposit]) {
|
||||
event {
|
||||
... on Deposit {
|
||||
...DepositFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const useDeposits = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const { data, loading, error, subscribeToMore } = useQuery<
|
||||
Deposits,
|
||||
DepositsVariables
|
||||
>(DEPOSITS_QUERY, {
|
||||
variables: { partyId: keypair?.pub || '' },
|
||||
skip: !keypair?.pub,
|
||||
});
|
||||
|
||||
const deposits = useMemo(() => {
|
||||
if (!data?.party?.depositsConnection.edges?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
compact(data.party?.depositsConnection.edges?.map((d) => d?.node)),
|
||||
['createdTimestamp'],
|
||||
['desc']
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!keypair?.pub) return;
|
||||
|
||||
const unsub = subscribeToMore<DepositEventSub, DepositEventSubVariables>({
|
||||
document: DEPOSITS_BUS_EVENT_SUB,
|
||||
variables: { partyId: keypair?.pub },
|
||||
updateQuery,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [keypair?.pub, subscribeToMore]);
|
||||
|
||||
return { data, loading, error, deposits };
|
||||
};
|
||||
|
||||
const updateQuery: UpdateQueryFn<
|
||||
Deposits,
|
||||
DepositEventSubVariables,
|
||||
DepositEventSub
|
||||
> = (prev, { subscriptionData, variables }) => {
|
||||
console.log(subscriptionData);
|
||||
if (!subscriptionData.data.busEvents?.length || !variables?.partyId) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const curr =
|
||||
compact(prev.party?.depositsConnection.edges?.map((e) => e?.node)) || [];
|
||||
const incoming = subscriptionData.data.busEvents
|
||||
.map((e) => e.event)
|
||||
.filter(isDepositEvent);
|
||||
|
||||
const deposits = uniqBy([...incoming, ...curr], 'id');
|
||||
|
||||
if (!prev.party) {
|
||||
return {
|
||||
...prev,
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: variables?.partyId,
|
||||
depositsConnection: {
|
||||
__typename: 'DepositsConnection',
|
||||
edges: deposits.map((d) => ({ __typename: 'DepositEdge', node: d })),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
party: {
|
||||
...prev.party,
|
||||
id: variables?.partyId,
|
||||
depositsConnection: {
|
||||
__typename: 'DepositsConnection',
|
||||
edges: deposits.map((d) => ({ __typename: 'DepositEdge', node: d })),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const isDepositEvent = (
|
||||
event: DepositEventSub_busEvents_event
|
||||
): event is DepositEventSub_busEvents_event_Deposit => {
|
||||
if (event.__typename === 'Deposit') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
@ -160,7 +160,7 @@ describe('ProposalForm', () => {
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||
'Proposal rejected'
|
||||
'Proposal submitted'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
getProposalDialogTitle,
|
||||
} from '../utils';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
|
||||
export interface FormFields {
|
||||
proposalData: string;
|
||||
@ -24,7 +23,7 @@ export const ProposalForm = () => {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm<FormFields>();
|
||||
const { finalizedProposal, submit, TransactionDialog } = useProposalSubmit();
|
||||
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||
|
||||
const hasError = Boolean(errors.proposalData?.message);
|
||||
|
||||
@ -63,31 +62,23 @@ export const ProposalForm = () => {
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<span className="my-20">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="proposal-submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||
</Button>
|
||||
</span>
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<TransactionDialog
|
||||
title={t('Proposal rejected')}
|
||||
intent={getProposalDialogIntent(ProposalState.STATE_REJECTED)}
|
||||
icon={getProposalDialogIcon(ProposalState.STATE_REJECTED)}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="proposal-submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||
</Button>
|
||||
<Dialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
>
|
||||
{finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
</TransactionDialog>
|
||||
) : (
|
||||
<TransactionDialog
|
||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||
/>
|
||||
)}
|
||||
) : undefined}
|
||||
</Dialog>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -10,8 +10,7 @@ export const useProposalSubmit = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const waitForProposalEvent = useProposalEvent();
|
||||
|
||||
const { send, transaction, setComplete, TransactionDialog } =
|
||||
useVegaTransaction();
|
||||
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
|
||||
|
||||
const [finalizedProposal, setFinalizedProposal] =
|
||||
useState<ProposalEvent_busEvents_event_Proposal | null>(null);
|
||||
@ -52,7 +51,7 @@ export const useProposalSubmit = () => {
|
||||
return {
|
||||
transaction,
|
||||
finalizedProposal,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
submit,
|
||||
};
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
||||
ref={ref}
|
||||
setEditOrder={setEditOrder}
|
||||
/>
|
||||
<orderCancel.TransactionDialog
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||
>
|
||||
@ -70,15 +70,15 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
</orderCancel.TransactionDialog>
|
||||
<orderEdit.TransactionDialog
|
||||
</orderCancel.Dialog>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
>
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
</orderEdit.TransactionDialog>
|
||||
</orderEdit.Dialog>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
|
@ -21,7 +21,7 @@ export const useOrderCancel = () => {
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
@ -62,7 +62,7 @@ export const useOrderCancel = () => {
|
||||
return {
|
||||
transaction,
|
||||
cancelledOrder,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
cancel,
|
||||
reset,
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ export const useOrderEdit = (order: OrderFields | null) => {
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderEvent = useOrderEvent();
|
||||
@ -71,7 +71,7 @@ export const useOrderEdit = (order: OrderFields | null) => {
|
||||
return {
|
||||
transaction,
|
||||
updatedOrder,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
edit,
|
||||
reset,
|
||||
};
|
||||
|
@ -102,7 +102,7 @@ export const useOrderSubmit = () => {
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const [finalizedOrder, setFinalizedOrder] =
|
||||
@ -158,7 +158,7 @@ export const useOrderSubmit = () => {
|
||||
return {
|
||||
transaction,
|
||||
finalizedOrder,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
submit,
|
||||
reset,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ const getSymbols = (positions: Position[]) =>
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const assetSymbols = useRef<string[] | undefined>();
|
||||
const { submit, TransactionDialog } = useClosePosition();
|
||||
const { submit, Dialog } = useClosePosition();
|
||||
const onClose = useCallback(
|
||||
(position: Position) => {
|
||||
submit(position);
|
||||
@ -55,9 +55,9 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
/>
|
||||
))}
|
||||
</AsyncRenderer>
|
||||
<TransactionDialog>
|
||||
<Dialog>
|
||||
<p>Your position was not closed! This is still not implemented. </p>
|
||||
</TransactionDialog>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ export const useClosePosition = () => {
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
@ -57,7 +57,7 @@ export const useClosePosition = () => {
|
||||
|
||||
return {
|
||||
transaction,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
submit,
|
||||
reset,
|
||||
};
|
||||
|
@ -34,6 +34,9 @@ export class CollateralBridge {
|
||||
get_withdraw_threshold(assetSource: string) {
|
||||
return this.contract.get_withdraw_threshold(assetSource);
|
||||
}
|
||||
default_withdraw_delay() {
|
||||
return this.contract.default_withdraw_delay();
|
||||
}
|
||||
withdraw_asset(
|
||||
assetSource: string,
|
||||
amount: string,
|
||||
|
@ -1,2 +1,30 @@
|
||||
import type { Get } from 'type-fest';
|
||||
import type {
|
||||
ICellRendererParams,
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
|
||||
export * from './ag-grid-lazy';
|
||||
export * from './ag-grid-dynamic';
|
||||
|
||||
type Field = string | readonly string[];
|
||||
|
||||
type RowHelper<TObj, TRow, TField extends Field> = Omit<
|
||||
TObj,
|
||||
'data' | 'value'
|
||||
> & {
|
||||
data: TRow;
|
||||
value: Get<TRow, TField>;
|
||||
};
|
||||
|
||||
export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
|
||||
ValueFormatterParams,
|
||||
TRow,
|
||||
TField
|
||||
>;
|
||||
|
||||
export type VegaICellRendererParams<TRow, TField extends Field> = RowHelper<
|
||||
ICellRendererParams,
|
||||
TRow,
|
||||
TField
|
||||
>;
|
||||
|
@ -102,7 +102,7 @@ export const useVegaTransaction = () => {
|
||||
[sendTx, setTransaction, reset]
|
||||
);
|
||||
|
||||
const TransactionDialog = useMemo(() => {
|
||||
const Dialog = useMemo(() => {
|
||||
return (props: DialogProps) => (
|
||||
<VegaTransactionDialog
|
||||
{...props}
|
||||
@ -122,7 +122,7 @@ export const useVegaTransaction = () => {
|
||||
reset,
|
||||
setComplete,
|
||||
setTransaction,
|
||||
TransactionDialog,
|
||||
Dialog,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -43,6 +43,7 @@ export const VegaTransactionDialog = ({
|
||||
intent={computedIntent}
|
||||
title={computedTitle}
|
||||
icon={computedIcon}
|
||||
size="small"
|
||||
>
|
||||
{content}
|
||||
</Dialog>
|
||||
@ -59,8 +60,9 @@ interface VegaDialogProps {
|
||||
export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
|
||||
let content = null;
|
||||
if (transaction.status === VegaTxStatus.Requested) {
|
||||
return (
|
||||
content = (
|
||||
<p data-testid={transaction.status}>
|
||||
{t(
|
||||
'Please open your wallet application and confirm or reject the transaction'
|
||||
@ -70,7 +72,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Error) {
|
||||
return (
|
||||
content = (
|
||||
<div data-testid={transaction.status}>
|
||||
<p>{transaction.error && formatLabel(transaction.error)}</p>
|
||||
{transaction.details && (
|
||||
@ -81,7 +83,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Pending) {
|
||||
return (
|
||||
content = (
|
||||
<div data-testid={transaction.status}>
|
||||
<p className="break-all">
|
||||
{t('Please wait for your transaction to be confirmed')} -
|
||||
@ -102,7 +104,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
}
|
||||
|
||||
if (transaction.status === VegaTxStatus.Complete) {
|
||||
return (
|
||||
content = (
|
||||
<div data-testid={transaction.status}>
|
||||
<p className="break-all">
|
||||
{t('Your transaction has been confirmed')} -
|
||||
@ -122,7 +124,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return <div className="text-sm">{content}</div>;
|
||||
};
|
||||
|
||||
const getIntent = (transaction: VegaTxState) => {
|
||||
@ -160,7 +162,11 @@ const getIcon = (transaction: VegaTxState) => {
|
||||
case VegaTxStatus.Requested:
|
||||
return <Icon name="hand-up" />;
|
||||
case VegaTxStatus.Pending:
|
||||
return <Loader size="small" />;
|
||||
return (
|
||||
<span className="mt-1">
|
||||
<Loader size="small" />
|
||||
</span>
|
||||
);
|
||||
case VegaTxStatus.Error:
|
||||
return <Icon name="warning-sign" />;
|
||||
case VegaTxStatus.Complete:
|
||||
|
@ -6,7 +6,7 @@ export * from './lib/use-token-decimals';
|
||||
export * from './lib/use-ethereum-config';
|
||||
export * from './lib/use-ethereum-read-contract';
|
||||
export * from './lib/use-ethereum-transaction';
|
||||
export * from './lib/transaction-dialog';
|
||||
export * from './lib/ethereum-transaction-dialog';
|
||||
export * from './lib/web3-provider';
|
||||
export * from './lib/web3-connect-dialog';
|
||||
export * from './lib/web3-wallet-input';
|
||||
|
@ -3,8 +3,8 @@ import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { EthereumError } from '../ethereum-error';
|
||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||
import type { TransactionDialogProps } from './transaction-dialog';
|
||||
import { TransactionDialog } from './transaction-dialog';
|
||||
import type { EthereumTransactionDialogProps } from './ethereum-transaction-dialog';
|
||||
import { EthereumTransactionDialog } from './ethereum-transaction-dialog';
|
||||
|
||||
jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({
|
||||
@ -12,11 +12,11 @@ jest.mock('@web3-react/core', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
let props: TransactionDialogProps;
|
||||
let props: EthereumTransactionDialogProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
name: 'test',
|
||||
title: 'test',
|
||||
onChange: jest.fn(),
|
||||
transaction: {
|
||||
status: EthTxStatus.Default,
|
||||
@ -29,9 +29,11 @@ beforeEach(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const generateJsx = (moreProps?: PartialDeep<TransactionDialogProps>) => {
|
||||
const generateJsx = (
|
||||
moreProps?: PartialDeep<EthereumTransactionDialogProps>
|
||||
) => {
|
||||
const mergedProps = merge(props, moreProps);
|
||||
return <TransactionDialog {...mergedProps} />;
|
||||
return <EthereumTransactionDialog {...mergedProps} />;
|
||||
};
|
||||
|
||||
it('Opens when tx starts and closes if the user rejects the tx', () => {
|
||||
@ -57,7 +59,7 @@ it('Opens when tx starts and closes if the user rejects the tx', () => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Doesn\t repoen if user dismissed the dialog', () => {
|
||||
it("Doesn't repoen if user dismissed the dialog", () => {
|
||||
const { container, rerender } = render(
|
||||
generateJsx({ transaction: { status: EthTxStatus.Pending } })
|
||||
);
|
||||
@ -87,7 +89,7 @@ it('Dialog states', () => {
|
||||
transaction: { status: EthTxStatus.Pending, confirmations: 0 },
|
||||
})
|
||||
);
|
||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${props.title} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Awaiting Ethereum transaction 0/1 confirmations...')
|
||||
@ -100,7 +102,7 @@ it('Dialog states', () => {
|
||||
transaction: { status: EthTxStatus.Complete, confirmations: 1 },
|
||||
})
|
||||
);
|
||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${props.title} pending`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
||||
|
||||
@ -113,7 +115,7 @@ it('Dialog states', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText(`${props.name} complete`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${props.title} complete`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Transaction confirmed')).toBeInTheDocument();
|
||||
|
||||
@ -128,6 +130,6 @@ it('Dialog states', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${props.title} failed`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`Error: ${reason}`)).toBeInTheDocument();
|
||||
});
|
@ -0,0 +1,125 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { isEthereumError } from '../ethereum-error';
|
||||
import type { EthTxState, TxError } from '../use-ethereum-transaction';
|
||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
||||
|
||||
export interface EthereumTransactionDialogProps {
|
||||
title: string;
|
||||
onChange: (isOpen: boolean) => void;
|
||||
transaction: EthTxState;
|
||||
// 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
|
||||
requiredConfirmations?: number;
|
||||
}
|
||||
|
||||
export const EthereumTransactionDialog = ({
|
||||
onChange,
|
||||
title,
|
||||
transaction,
|
||||
requiredConfirmations = 1,
|
||||
}: EthereumTransactionDialogProps) => {
|
||||
const { status, error, confirmations, txHash } = transaction;
|
||||
return (
|
||||
<Dialog
|
||||
open={transaction.dialogOpen}
|
||||
onChange={onChange}
|
||||
size="small"
|
||||
{...getWrapperProps(title, status)}
|
||||
>
|
||||
<TransactionContent
|
||||
status={status}
|
||||
error={error}
|
||||
txHash={txHash}
|
||||
confirmations={confirmations}
|
||||
requiredConfirmations={requiredConfirmations}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const TransactionContent = ({
|
||||
status,
|
||||
error,
|
||||
txHash,
|
||||
confirmations,
|
||||
requiredConfirmations = 1,
|
||||
}: {
|
||||
status: EthTxStatus;
|
||||
error: TxError | null;
|
||||
txHash: string | null;
|
||||
confirmations: number;
|
||||
requiredConfirmations?: number;
|
||||
}) => {
|
||||
if (status === EthTxStatus.Error) {
|
||||
let errorMessage = '';
|
||||
|
||||
if (isEthereumError(error)) {
|
||||
errorMessage = error.reason;
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="break-all">
|
||||
{t('Error')}: {errorMessage}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<ConfirmRow status={status} />
|
||||
<TxRow
|
||||
status={status}
|
||||
txHash={txHash}
|
||||
confirmations={confirmations}
|
||||
requiredConfirmations={requiredConfirmations}
|
||||
highlightComplete={false}
|
||||
/>
|
||||
<ConfirmationEventRow status={status} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getWrapperProps = (title: string, status: EthTxStatus) => {
|
||||
const propsMap = {
|
||||
[EthTxStatus.Default]: {
|
||||
title: '',
|
||||
icon: null,
|
||||
intent: undefined,
|
||||
},
|
||||
[EthTxStatus.Error]: {
|
||||
title: t(`${title} failed`),
|
||||
icon: <Icon name="warning-sign" />,
|
||||
intent: Intent.Danger,
|
||||
},
|
||||
[EthTxStatus.Requested]: {
|
||||
title: t('Confirm transaction'),
|
||||
icon: <Icon name="hand-up" />,
|
||||
intent: Intent.Warning,
|
||||
},
|
||||
[EthTxStatus.Pending]: {
|
||||
title: t(`${title} pending`),
|
||||
icon: (
|
||||
<span className="mt-1">
|
||||
<Loader size="small" />
|
||||
</span>
|
||||
),
|
||||
intent: Intent.None,
|
||||
},
|
||||
[EthTxStatus.Complete]: {
|
||||
title: t(`${title} pending`),
|
||||
icon: <Loader size="small" />,
|
||||
intent: Intent.None,
|
||||
},
|
||||
[EthTxStatus.Confirmed]: {
|
||||
title: t(`${title} complete`),
|
||||
icon: <Icon name="tick" />,
|
||||
intent: Intent.Success,
|
||||
},
|
||||
};
|
||||
|
||||
return propsMap[status];
|
||||
};
|
1
libs/web3/src/lib/ethereum-transaction-dialog/index.ts
Normal file
1
libs/web3/src/lib/ethereum-transaction-dialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ethereum-transaction-dialog';
|
@ -1 +0,0 @@
|
||||
export * from './transaction-dialog';
|
@ -1,115 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { isEthereumError } from '../ethereum-error';
|
||||
import type { EthTxState } from '../use-ethereum-transaction';
|
||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
||||
|
||||
export interface TransactionDialogProps {
|
||||
name: string;
|
||||
onChange: (isOpen: boolean) => void;
|
||||
transaction: EthTxState;
|
||||
// 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
|
||||
requiredConfirmations?: number;
|
||||
}
|
||||
|
||||
export const TransactionDialog = ({
|
||||
onChange,
|
||||
name,
|
||||
transaction,
|
||||
requiredConfirmations = 1,
|
||||
}: TransactionDialogProps) => {
|
||||
const { status, error, confirmations, txHash } = transaction;
|
||||
|
||||
const renderContent = () => {
|
||||
if (status === EthTxStatus.Error) {
|
||||
const classNames = 'break-all text-black dark:text-white';
|
||||
if (isEthereumError(error)) {
|
||||
return (
|
||||
<p className={classNames}>
|
||||
{t('Error')}: {error.reason}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return (
|
||||
<p className={classNames}>
|
||||
{t('Error')}: {error.message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={classNames}>
|
||||
{t('Error')}: {t('Unknown error')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmRow status={status} />
|
||||
<TxRow
|
||||
status={status}
|
||||
txHash={txHash}
|
||||
confirmations={confirmations}
|
||||
requiredConfirmations={requiredConfirmations}
|
||||
highlightComplete={false}
|
||||
/>
|
||||
<ConfirmationEventRow status={status} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getWrapperProps = () => {
|
||||
const propsMap = {
|
||||
[EthTxStatus.Default]: {
|
||||
title: '',
|
||||
icon: null,
|
||||
intent: undefined,
|
||||
},
|
||||
[EthTxStatus.Error]: {
|
||||
title: t(`${name} failed`),
|
||||
icon: <Icon name="warning-sign" />,
|
||||
intent: Intent.Danger,
|
||||
},
|
||||
[EthTxStatus.Requested]: {
|
||||
title: t('Confirm transaction'),
|
||||
icon: <Icon name="hand-up" />,
|
||||
intent: Intent.Warning,
|
||||
},
|
||||
[EthTxStatus.Pending]: {
|
||||
title: t(`${name} pending`),
|
||||
icon: <Loader size="small" />,
|
||||
intent: Intent.None,
|
||||
},
|
||||
[EthTxStatus.Complete]: {
|
||||
title: t(`${name} pending`),
|
||||
icon: <Loader size="small" />,
|
||||
intent: Intent.None,
|
||||
},
|
||||
[EthTxStatus.Confirmed]: {
|
||||
title: t(`${name} complete`),
|
||||
icon: <Icon name="tick" />,
|
||||
intent: Intent.Success,
|
||||
},
|
||||
};
|
||||
|
||||
return propsMap[status];
|
||||
};
|
||||
|
||||
const { intent, title, icon } = getWrapperProps();
|
||||
return (
|
||||
<Dialog
|
||||
open={transaction.dialogOpen}
|
||||
onChange={onChange}
|
||||
intent={intent}
|
||||
title={title}
|
||||
icon={icon}
|
||||
>
|
||||
{renderContent()}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -4,7 +4,7 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import type { EthereumError } from './ethereum-error';
|
||||
import { isExpectedEthereumError } from './ethereum-error';
|
||||
import { isEthereumError } from './ethereum-error';
|
||||
import { TransactionDialog } from './transaction-dialog';
|
||||
import { EthereumTransactionDialog } from './ethereum-transaction-dialog';
|
||||
|
||||
export enum EthTxStatus {
|
||||
Default = 'Default',
|
||||
@ -152,8 +152,8 @@ export const useEthereumTransaction = <
|
||||
|
||||
const Dialog = useMemo(() => {
|
||||
return () => (
|
||||
<TransactionDialog
|
||||
name={formatLabel(methodName as string)}
|
||||
<EthereumTransactionDialog
|
||||
title={formatLabel(methodName as string)}
|
||||
onChange={() => {
|
||||
reset();
|
||||
}}
|
||||
|
@ -1,7 +1,12 @@
|
||||
export * from './lib/withdrawal-dialogs';
|
||||
export * from './lib/withdraw-form';
|
||||
export * from './lib/withdraw-form-container';
|
||||
export * from './lib/withdraw-manager';
|
||||
export * from './lib/withdrawals-table';
|
||||
export * from './lib/withdrawal-feedback';
|
||||
export * from './lib/use-complete-withdraw';
|
||||
export * from './lib/use-withdraw';
|
||||
export * from './lib/use-create-withdraw';
|
||||
export * from './lib/use-verify-withdrawal';
|
||||
export * from './lib/use-withdrawals';
|
||||
export * from './lib/__generated__/Withdrawals';
|
||||
export * from './lib/__generated__/WithdrawalFields';
|
||||
|
46
libs/withdraws/src/lib/__generated__/AssetFields.ts
generated
Normal file
46
libs/withdraws/src/lib/__generated__/AssetFields.ts
generated
Normal file
@ -0,0 +1,46 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: AssetFields
|
||||
// ====================================================
|
||||
|
||||
export interface AssetFields_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface AssetFields_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type AssetFields_source = AssetFields_source_BuiltinAsset | AssetFields_source_ERC20;
|
||||
|
||||
export interface AssetFields {
|
||||
__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. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: AssetFields_source;
|
||||
}
|
@ -6,10 +6,10 @@
|
||||
import { AccountType } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: WithdrawPageQuery
|
||||
// GraphQL query operation: WithdrawFormQuery
|
||||
// ====================================================
|
||||
|
||||
export interface WithdrawPageQuery_party_withdrawals {
|
||||
export interface WithdrawFormQuery_party_withdrawals {
|
||||
__typename: "Withdrawal";
|
||||
/**
|
||||
* The Vega internal ID of the withdrawal
|
||||
@ -21,7 +21,7 @@ export interface WithdrawPageQuery_party_withdrawals {
|
||||
txHash: string | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_party_accounts_asset {
|
||||
export interface WithdrawFormQuery_party_accounts_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
@ -33,7 +33,7 @@ export interface WithdrawPageQuery_party_accounts_asset {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_party_accounts {
|
||||
export interface WithdrawFormQuery_party_accounts {
|
||||
__typename: "Account";
|
||||
/**
|
||||
* Account type (General, Margin, etc)
|
||||
@ -46,10 +46,10 @@ export interface WithdrawPageQuery_party_accounts {
|
||||
/**
|
||||
* Asset, the 'currency'
|
||||
*/
|
||||
asset: WithdrawPageQuery_party_accounts_asset;
|
||||
asset: WithdrawFormQuery_party_accounts_asset;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_party {
|
||||
export interface WithdrawFormQuery_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
@ -58,18 +58,18 @@ export interface WithdrawPageQuery_party {
|
||||
/**
|
||||
* The list of all withdrawals initiated by the party
|
||||
*/
|
||||
withdrawals: WithdrawPageQuery_party_withdrawals[] | null;
|
||||
withdrawals: WithdrawFormQuery_party_withdrawals[] | null;
|
||||
/**
|
||||
* Collateral accounts relating to a party
|
||||
*/
|
||||
accounts: WithdrawPageQuery_party_accounts[] | null;
|
||||
accounts: WithdrawFormQuery_party_accounts[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_assetsConnection_edges_node_source_BuiltinAsset {
|
||||
export interface WithdrawFormQuery_assetsConnection_edges_node_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_assetsConnection_edges_node_source_ERC20 {
|
||||
export interface WithdrawFormQuery_assetsConnection_edges_node_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
@ -77,9 +77,9 @@ export interface WithdrawPageQuery_assetsConnection_edges_node_source_ERC20 {
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawPageQuery_assetsConnection_edges_node_source = WithdrawPageQuery_assetsConnection_edges_node_source_BuiltinAsset | WithdrawPageQuery_assetsConnection_edges_node_source_ERC20;
|
||||
export type WithdrawFormQuery_assetsConnection_edges_node_source = WithdrawFormQuery_assetsConnection_edges_node_source_BuiltinAsset | WithdrawFormQuery_assetsConnection_edges_node_source_ERC20;
|
||||
|
||||
export interface WithdrawPageQuery_assetsConnection_edges_node {
|
||||
export interface WithdrawFormQuery_assetsConnection_edges_node {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
@ -100,33 +100,33 @@ export interface WithdrawPageQuery_assetsConnection_edges_node {
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawPageQuery_assetsConnection_edges_node_source;
|
||||
source: WithdrawFormQuery_assetsConnection_edges_node_source;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_assetsConnection_edges {
|
||||
export interface WithdrawFormQuery_assetsConnection_edges {
|
||||
__typename: "AssetEdge";
|
||||
node: WithdrawPageQuery_assetsConnection_edges_node;
|
||||
node: WithdrawFormQuery_assetsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery_assetsConnection {
|
||||
export interface WithdrawFormQuery_assetsConnection {
|
||||
__typename: "AssetsConnection";
|
||||
/**
|
||||
* The assets
|
||||
*/
|
||||
edges: (WithdrawPageQuery_assetsConnection_edges | null)[] | null;
|
||||
edges: (WithdrawFormQuery_assetsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQuery {
|
||||
export interface WithdrawFormQuery {
|
||||
/**
|
||||
* An entity that is trading on the Vega network
|
||||
*/
|
||||
party: WithdrawPageQuery_party | null;
|
||||
party: WithdrawFormQuery_party | null;
|
||||
/**
|
||||
* The list of all assets in use in the Vega network or the specified asset if ID is provided
|
||||
*/
|
||||
assetsConnection: WithdrawPageQuery_assetsConnection;
|
||||
assetsConnection: WithdrawFormQuery_assetsConnection;
|
||||
}
|
||||
|
||||
export interface WithdrawPageQueryVariables {
|
||||
export interface WithdrawFormQueryVariables {
|
||||
partyId: string;
|
||||
}
|
@ -13,12 +13,30 @@ export interface WithdrawalEvent_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" | "Deposit" | "OracleSpec" | "LiquidityProvision";
|
||||
}
|
||||
|
||||
export interface WithdrawalEvent_busEvents_event_Withdrawal_asset_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawalEvent_busEvents_event_Withdrawal_asset_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawalEvent_busEvents_event_Withdrawal_asset_source = WithdrawalEvent_busEvents_event_Withdrawal_asset_source_BuiltinAsset | WithdrawalEvent_busEvents_event_Withdrawal_asset_source_ERC20;
|
||||
|
||||
export interface WithdrawalEvent_busEvents_event_Withdrawal_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
@ -27,6 +45,10 @@ export interface WithdrawalEvent_busEvents_event_Withdrawal_asset {
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawalEvent_busEvents_event_Withdrawal_asset_source;
|
||||
}
|
||||
|
||||
export interface WithdrawalEvent_busEvents_event_Withdrawal_details {
|
||||
|
@ -9,12 +9,30 @@ import { WithdrawalStatus } from "@vegaprotocol/types";
|
||||
// GraphQL fragment: WithdrawalFields
|
||||
// ====================================================
|
||||
|
||||
export interface WithdrawalFields_asset_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface WithdrawalFields_asset_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type WithdrawalFields_asset_source = WithdrawalFields_asset_source_BuiltinAsset | WithdrawalFields_asset_source_ERC20;
|
||||
|
||||
export interface WithdrawalFields_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
@ -23,6 +41,10 @@ export interface WithdrawalFields_asset {
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: WithdrawalFields_asset_source;
|
||||
}
|
||||
|
||||
export interface WithdrawalFields_details {
|
||||
|
47
libs/withdraws/src/lib/__generated__/Withdrawals.ts
generated
47
libs/withdraws/src/lib/__generated__/Withdrawals.ts
generated
@ -9,12 +9,30 @@ import { WithdrawalStatus } from "@vegaprotocol/types";
|
||||
// GraphQL query operation: Withdrawals
|
||||
// ====================================================
|
||||
|
||||
export interface Withdrawals_party_withdrawals_asset {
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges_node_asset_source_BuiltinAsset {
|
||||
__typename: "BuiltinAsset";
|
||||
}
|
||||
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges_node_asset_source_ERC20 {
|
||||
__typename: "ERC20";
|
||||
/**
|
||||
* The address of the ERC20 contract
|
||||
*/
|
||||
contractAddress: string;
|
||||
}
|
||||
|
||||
export type Withdrawals_party_withdrawalsConnection_edges_node_asset_source = Withdrawals_party_withdrawalsConnection_edges_node_asset_source_BuiltinAsset | Withdrawals_party_withdrawalsConnection_edges_node_asset_source_ERC20;
|
||||
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges_node_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The ID of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
@ -23,9 +41,13 @@ export interface Withdrawals_party_withdrawals_asset {
|
||||
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
|
||||
*/
|
||||
decimals: number;
|
||||
/**
|
||||
* The origin source of the asset (e.g: an ERC20 asset)
|
||||
*/
|
||||
source: Withdrawals_party_withdrawalsConnection_edges_node_asset_source;
|
||||
}
|
||||
|
||||
export interface Withdrawals_party_withdrawals_details {
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges_node_details {
|
||||
__typename: "Erc20WithdrawalDetails";
|
||||
/**
|
||||
* The ethereum address of the receiver of the asset funds
|
||||
@ -33,7 +55,7 @@ export interface Withdrawals_party_withdrawals_details {
|
||||
receiverAddress: string;
|
||||
}
|
||||
|
||||
export interface Withdrawals_party_withdrawals {
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges_node {
|
||||
__typename: "Withdrawal";
|
||||
/**
|
||||
* The Vega internal ID of the withdrawal
|
||||
@ -50,7 +72,7 @@ export interface Withdrawals_party_withdrawals {
|
||||
/**
|
||||
* The asset to be withdrawn
|
||||
*/
|
||||
asset: Withdrawals_party_withdrawals_asset;
|
||||
asset: Withdrawals_party_withdrawalsConnection_edges_node_asset;
|
||||
/**
|
||||
* RFC3339Nano time at which the withdrawal was created
|
||||
*/
|
||||
@ -66,13 +88,26 @@ export interface Withdrawals_party_withdrawals {
|
||||
/**
|
||||
* Foreign chain specific details about the withdrawal
|
||||
*/
|
||||
details: Withdrawals_party_withdrawals_details | null;
|
||||
details: Withdrawals_party_withdrawalsConnection_edges_node_details | null;
|
||||
/**
|
||||
* Whether or the not the withdrawal is being processed on Ethereum
|
||||
*/
|
||||
pendingOnForeignChain: boolean;
|
||||
}
|
||||
|
||||
export interface Withdrawals_party_withdrawalsConnection_edges {
|
||||
__typename: "WithdrawalEdge";
|
||||
node: Withdrawals_party_withdrawalsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface Withdrawals_party_withdrawalsConnection {
|
||||
__typename: "WithdrawalsConnection";
|
||||
/**
|
||||
* The withdrawals
|
||||
*/
|
||||
edges: (Withdrawals_party_withdrawalsConnection_edges | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Withdrawals_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
@ -82,7 +117,7 @@ export interface Withdrawals_party {
|
||||
/**
|
||||
* The list of all withdrawals initiated by the party
|
||||
*/
|
||||
withdrawals: Withdrawals_party_withdrawals[] | null;
|
||||
withdrawalsConnection: Withdrawals_party_withdrawalsConnection;
|
||||
}
|
||||
|
||||
export interface Withdrawals {
|
||||
|
@ -3,7 +3,7 @@ import { AccountType, WithdrawalStatus } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Account } from './types';
|
||||
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||
import type { Withdrawals_party_withdrawalsConnection_edges_node } from './__generated__/Withdrawals';
|
||||
|
||||
export const generateAsset = (override?: PartialDeep<Asset>) => {
|
||||
const defaultAsset: Asset = {
|
||||
@ -33,8 +33,8 @@ export const generateAccount = (override?: PartialDeep<Account>) => {
|
||||
};
|
||||
|
||||
export const generateWithdrawal = (
|
||||
override?: PartialDeep<Withdrawals_party_withdrawals>
|
||||
): Withdrawals_party_withdrawals => {
|
||||
override?: PartialDeep<Withdrawals_party_withdrawalsConnection_edges_node>
|
||||
): Withdrawals_party_withdrawalsConnection_edges_node => {
|
||||
return merge(
|
||||
{
|
||||
__typename: 'Withdrawal',
|
||||
@ -43,9 +43,14 @@ export const generateWithdrawal = (
|
||||
amount: '100',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
name: 'asset-name',
|
||||
id: 'asset-id',
|
||||
symbol: 'asset-symbol',
|
||||
decimals: 2,
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x123',
|
||||
},
|
||||
},
|
||||
createdTimestamp: '2022-04-20T00:00:00',
|
||||
withdrawnTimestamp: null,
|
||||
|
@ -10,13 +10,17 @@ import * as web3 from '@vegaprotocol/web3';
|
||||
import * as sentry from '@sentry/react';
|
||||
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
||||
|
||||
jest.mock('@vegaprotocol/web3', () => ({
|
||||
useBridgeContract: jest.fn().mockReturnValue({
|
||||
withdraw_asset: jest.fn(),
|
||||
isNewContract: true,
|
||||
}),
|
||||
useEthereumTransaction: jest.fn(),
|
||||
}));
|
||||
jest.mock('@vegaprotocol/web3', () => {
|
||||
const orig = jest.requireActual('@vegaprotocol/web3');
|
||||
return {
|
||||
...orig,
|
||||
useBridgeContract: jest.fn().mockReturnValue({
|
||||
withdraw_asset: jest.fn(),
|
||||
isNewContract: true,
|
||||
}),
|
||||
useEthereumTransaction: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function setup(mocks?: MockedResponse[]) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { gql, useApolloClient } from '@apollo/client';
|
||||
import { captureException } from '@sentry/react';
|
||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
import { useBridgeContract, useEthereumTransaction } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useBridgeContract,
|
||||
useEthereumTransaction,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import type {
|
||||
@ -21,7 +25,7 @@ export const useCompleteWithdraw = () => {
|
||||
const { query, cache } = useApolloClient();
|
||||
const contract = useBridgeContract();
|
||||
const [id, setId] = useState('');
|
||||
const { transaction, perform, Dialog } = useEthereumTransaction<
|
||||
const { transaction, perform, reset, Dialog } = useEthereumTransaction<
|
||||
CollateralBridge,
|
||||
'withdraw_asset'
|
||||
>(contract, 'withdraw_asset');
|
||||
@ -29,6 +33,7 @@ export const useCompleteWithdraw = () => {
|
||||
const submit = useCallback(
|
||||
async (withdrawalId: string) => {
|
||||
setId(withdrawalId);
|
||||
|
||||
try {
|
||||
if (!contract) {
|
||||
return;
|
||||
@ -39,6 +44,7 @@ export const useCompleteWithdraw = () => {
|
||||
});
|
||||
|
||||
const approval = res.data.erc20WithdrawalApproval;
|
||||
|
||||
if (!approval) {
|
||||
throw new Error('Could not retrieve withdrawal approval');
|
||||
}
|
||||
@ -65,12 +71,13 @@ export const useCompleteWithdraw = () => {
|
||||
fragment: PENDING_WITHDRAWAL_FRAGMMENT,
|
||||
data: {
|
||||
__typename: 'Withdrawal',
|
||||
pendingOnForeignChain: true,
|
||||
pendingOnForeignChain:
|
||||
transaction.status === EthTxStatus.Pending ? true : false,
|
||||
txHash: transaction.txHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [cache, transaction.txHash, id]);
|
||||
}, [cache, transaction.status, transaction.txHash, id]);
|
||||
|
||||
return { transaction, Dialog, submit, withdrawalId: id };
|
||||
return { transaction, reset, Dialog, submit, withdrawalId: id };
|
||||
};
|
||||
|
197
libs/withdraws/src/lib/use-create-withdraw.spec.tsx
Normal file
197
libs/withdraws/src/lib/use-create-withdraw.spec.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import { useCreateWithdraw } from './use-create-withdraw';
|
||||
import type { Erc20Approval } from './__generated__/Erc20Approval';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
initialState,
|
||||
VegaTxStatus,
|
||||
VegaWalletContext,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { determineId } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
WithdrawalEvent,
|
||||
WithdrawalEvent_busEvents_event_Withdrawal,
|
||||
} from './__generated__/WithdrawalEvent';
|
||||
import { WITHDRAWAL_BUS_EVENT_SUB } from './use-withdrawals';
|
||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||
|
||||
function setup(
|
||||
vegaWalletContext: Partial<VegaWalletContextShape>,
|
||||
mocks?: MockedResponse[]
|
||||
) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider
|
||||
value={vegaWalletContext as VegaWalletContextShape}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useCreateWithdraw(), { wrapper });
|
||||
}
|
||||
|
||||
const txHash = 'tx-hash';
|
||||
const signature =
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909';
|
||||
const derivedWithdrawalId = determineId(signature);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const pubkey = '0x123';
|
||||
let mockSend: jest.Mock;
|
||||
let withdrawalInput: WithdrawalArgs;
|
||||
let withdrawalEvent: WithdrawalEvent_busEvents_event_Withdrawal;
|
||||
let mockERC20Approval: MockedResponse<Erc20Approval>;
|
||||
let mockWithdrawalEvent: MockedResponse<WithdrawalEvent>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSend = jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ txHash, tx: { signature: { value: signature } } })
|
||||
);
|
||||
withdrawalEvent = {
|
||||
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
|
||||
status: WithdrawalStatus.STATUS_OPEN,
|
||||
amount: '100',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
name: 'asset-name',
|
||||
symbol: 'asset-symbol',
|
||||
decimals: 2,
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
contractAddress: '0x123',
|
||||
},
|
||||
},
|
||||
createdTimestamp: '2022-07-05T14:25:47.815283706Z',
|
||||
withdrawnTimestamp: '2022-07-05T14:25:47.815283706Z',
|
||||
txHash: '0x123',
|
||||
details: {
|
||||
__typename: 'Erc20WithdrawalDetails',
|
||||
receiverAddress: '0x123',
|
||||
},
|
||||
pendingOnForeignChain: false,
|
||||
__typename: 'Withdrawal',
|
||||
};
|
||||
withdrawalInput = {
|
||||
amount: '100',
|
||||
asset: 'asset-id',
|
||||
receiverAddress: 'receiver-address',
|
||||
availableTimestamp: null,
|
||||
};
|
||||
mockERC20Approval = {
|
||||
request: {
|
||||
query: ERC20_APPROVAL_QUERY,
|
||||
variables: { withdrawalId: derivedWithdrawalId },
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
erc20WithdrawalApproval: {
|
||||
__typename: 'Erc20WithdrawalApproval',
|
||||
assetSource: 'asset-source',
|
||||
amount: '100',
|
||||
nonce: '1',
|
||||
signatures: 'signatures',
|
||||
targetAddress: 'targetAddress',
|
||||
expiry: 'expiry',
|
||||
creation: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
delay: 2000,
|
||||
};
|
||||
mockWithdrawalEvent = {
|
||||
request: {
|
||||
query: WITHDRAWAL_BUS_EVENT_SUB,
|
||||
variables: { partyId: pubkey },
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
event: withdrawalEvent,
|
||||
__typename: 'BusEvent',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
delay: 1000,
|
||||
};
|
||||
});
|
||||
|
||||
it('creates withdrawal and waits for approval creation', async () => {
|
||||
const { result } = setup(
|
||||
// @ts-ignore only need pub property from keypair
|
||||
{ sendTx: mockSend, keypair: { pub: pubkey } },
|
||||
[mockWithdrawalEvent, mockERC20Approval]
|
||||
);
|
||||
|
||||
expect(result.current.transaction).toEqual(initialState);
|
||||
expect(result.current.submit).toEqual(expect.any(Function));
|
||||
expect(result.current.reset).toEqual(expect.any(Function));
|
||||
expect(result.current.approval).toEqual(null);
|
||||
|
||||
act(() => {
|
||||
result.current.submit(withdrawalInput);
|
||||
});
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith({
|
||||
pubKey: pubkey,
|
||||
propagate: true,
|
||||
withdrawSubmission: {
|
||||
amount: withdrawalInput.amount,
|
||||
asset: withdrawalInput.asset,
|
||||
ext: {
|
||||
erc20: {
|
||||
receiverAddress: withdrawalInput.receiverAddress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Requested);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Pending);
|
||||
expect(result.current.transaction.dialogOpen).toBe(true);
|
||||
// Withdrawal event should not be found yet
|
||||
expect(result.current.withdrawal).toEqual(null);
|
||||
// Poll for erc20Approval should not be complete yet
|
||||
expect(result.current.approval).toEqual(null);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Advance time by delay plus interval length to ensure mock result is triggered
|
||||
// eslint-disable-next-line
|
||||
jest.advanceTimersByTime(mockWithdrawalEvent.delay! + 1000);
|
||||
});
|
||||
|
||||
expect(result.current.withdrawal).toEqual(withdrawalEvent);
|
||||
|
||||
await act(async () => {
|
||||
// Advance time by delay plus interval length to ensure mock result is triggered
|
||||
// eslint-disable-next-line
|
||||
jest.advanceTimersByTime(mockERC20Approval.delay! + 1000);
|
||||
});
|
||||
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Complete);
|
||||
expect(result.current.approval).toEqual(
|
||||
// @ts-ignore MockedRespones types inteferring
|
||||
mockERC20Approval.result.data.erc20WithdrawalApproval
|
||||
);
|
||||
});
|
74
libs/withdraws/src/lib/use-create-withdraw.ts
Normal file
74
libs/withdraws/src/lib/use-create-withdraw.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { determineId } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useWithdrawalApproval } from './use-withdrawal-approval';
|
||||
import { useWithdrawalEvent } from './use-withdrawal-event';
|
||||
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
||||
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
|
||||
|
||||
export interface WithdrawalArgs {
|
||||
amount: string;
|
||||
asset: string;
|
||||
receiverAddress: string;
|
||||
availableTimestamp: number | null;
|
||||
}
|
||||
|
||||
export const useCreateWithdraw = () => {
|
||||
const waitForWithdrawalApproval = useWithdrawalApproval();
|
||||
const waitForWithdrawal = useWithdrawalEvent();
|
||||
const [approval, setApproval] =
|
||||
useState<Erc20Approval_erc20WithdrawalApproval | null>(null);
|
||||
const [withdrawal, setWithdrawal] = useState<WithdrawalFields | null>(null);
|
||||
const [availableTimestamp, setAvailableTimestamp] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { keypair } = useVegaWallet();
|
||||
const { transaction, send, setComplete, reset, Dialog } =
|
||||
useVegaTransaction();
|
||||
|
||||
const submit = useCallback(
|
||||
async (withdrawal: WithdrawalArgs) => {
|
||||
if (!keypair) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAvailableTimestamp(withdrawal.availableTimestamp);
|
||||
|
||||
const res = await send({
|
||||
pubKey: keypair.pub,
|
||||
propagate: true,
|
||||
withdrawSubmission: {
|
||||
amount: withdrawal.amount,
|
||||
asset: withdrawal.asset,
|
||||
ext: {
|
||||
erc20: {
|
||||
receiverAddress: withdrawal.receiverAddress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
const id = determineId(res.signature);
|
||||
|
||||
const withdrawal = await waitForWithdrawal(id, keypair.pub);
|
||||
setWithdrawal(withdrawal);
|
||||
const approval = await waitForWithdrawalApproval(withdrawal.id);
|
||||
setApproval(approval);
|
||||
setComplete();
|
||||
}
|
||||
},
|
||||
[keypair, send, waitForWithdrawal, waitForWithdrawalApproval, setComplete]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
submit,
|
||||
reset,
|
||||
approval,
|
||||
withdrawal,
|
||||
availableTimestamp,
|
||||
Dialog,
|
||||
};
|
||||
};
|
21
libs/withdraws/src/lib/use-get-withdraw-delay.ts
Normal file
21
libs/withdraws/src/lib/use-get-withdraw-delay.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useBridgeContract } from '@vegaprotocol/web3';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Gets the delay in seconds thats required if the withdrawal amount is
|
||||
* over the withdrawal threshold (contract.get_withdraw_threshold)
|
||||
*/
|
||||
export const useGetWithdrawDelay = () => {
|
||||
const contract = useBridgeContract();
|
||||
const getDelay = useCallback(async () => {
|
||||
try {
|
||||
const res = await contract?.default_withdraw_delay();
|
||||
return res.toNumber();
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}, [contract]);
|
||||
|
||||
return getDelay;
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useBridgeContract, useEthereumReadContract } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const useGetWithdrawLimits = (asset?: Asset) => {
|
||||
const contract = useBridgeContract();
|
||||
const getLimits = useCallback(async () => {
|
||||
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
|
||||
return;
|
||||
}
|
||||
|
||||
return contract.get_withdraw_threshold(asset.source.contractAddress);
|
||||
}, [asset, contract]);
|
||||
|
||||
const {
|
||||
state: { data },
|
||||
} = useEthereumReadContract(getLimits);
|
||||
|
||||
if (!data || !asset) return null;
|
||||
|
||||
const value = new BigNumber(addDecimal(data.toString(), asset.decimals));
|
||||
const max = value.isEqualTo(0)
|
||||
? new BigNumber(Infinity)
|
||||
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
|
||||
return {
|
||||
max,
|
||||
};
|
||||
};
|
32
libs/withdraws/src/lib/use-get-withdraw-threshold.tsx
Normal file
32
libs/withdraws/src/lib/use-get-withdraw-threshold.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useBridgeContract } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
|
||||
/**
|
||||
* Returns a function to get the threshold amount for a withdrawal. If a withdrawal amount
|
||||
* is greater than this value it will incur a delay before being able to be completed. The delay is set
|
||||
* on the smart contract and can be retrieved using contract.default_withdraw_delay
|
||||
*/
|
||||
export const useGetWithdrawThreshold = () => {
|
||||
const contract = useBridgeContract();
|
||||
const getThreshold = useCallback(
|
||||
async (asset: Asset | undefined) => {
|
||||
if (!contract || asset?.source.__typename !== 'ERC20') {
|
||||
return new BigNumber(Infinity);
|
||||
}
|
||||
const res = await contract.get_withdraw_threshold(
|
||||
asset.source.contractAddress
|
||||
);
|
||||
const value = new BigNumber(addDecimal(res.toString(), asset.decimals));
|
||||
const threshold = value.isEqualTo(0)
|
||||
? new BigNumber(Infinity)
|
||||
: value.minus(new BigNumber(addDecimal('1', asset.decimals)));
|
||||
return threshold;
|
||||
},
|
||||
[contract]
|
||||
);
|
||||
|
||||
return getThreshold;
|
||||
};
|
135
libs/withdraws/src/lib/use-verify-withdrawal.ts
Normal file
135
libs/withdraws/src/lib/use-verify-withdrawal.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { captureException } from '@sentry/react';
|
||||
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { addDecimal, t } from '@vegaprotocol/react-helpers';
|
||||
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
|
||||
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import type {
|
||||
Erc20Approval,
|
||||
Erc20ApprovalVariables,
|
||||
} from './__generated__/Erc20Approval';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
export enum ApprovalStatus {
|
||||
Idle = 'Idle',
|
||||
Pending = 'Pending',
|
||||
Delayed = 'Delayed',
|
||||
Error = 'Error',
|
||||
Ready = 'Ready',
|
||||
}
|
||||
|
||||
export interface VerifyState {
|
||||
status: ApprovalStatus;
|
||||
message: string;
|
||||
threshold: BigNumber;
|
||||
completeTimestamp: number | null;
|
||||
dialogOpen: boolean;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
status: ApprovalStatus.Idle,
|
||||
message: '',
|
||||
threshold: new BigNumber(Infinity),
|
||||
completeTimestamp: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
export const useVerifyWithdrawal = () => {
|
||||
const client = useApolloClient();
|
||||
const getThreshold = useGetWithdrawThreshold();
|
||||
const getDelay = useGetWithdrawDelay();
|
||||
const [state, _setState] = useState<VerifyState>(initialState);
|
||||
|
||||
const setState = useCallback(
|
||||
(update: Partial<VerifyState>) => {
|
||||
_setState((curr) => ({
|
||||
...curr,
|
||||
...update,
|
||||
}));
|
||||
},
|
||||
[_setState]
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState(initialState);
|
||||
}, [setState]);
|
||||
|
||||
const verify = useCallback(
|
||||
async (withdrawal: WithdrawalFields) => {
|
||||
try {
|
||||
setState({ dialogOpen: true });
|
||||
|
||||
if (withdrawal.asset.source.__typename !== 'ERC20') {
|
||||
setState({
|
||||
status: ApprovalStatus.Error,
|
||||
message: t(
|
||||
`Invalid asset source: ${withdrawal.asset.source.__typename}`
|
||||
),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setState({
|
||||
status: ApprovalStatus.Pending,
|
||||
message: t('Verifying withdrawal approval'),
|
||||
});
|
||||
|
||||
const amount = new BigNumber(
|
||||
addDecimal(withdrawal.amount, withdrawal.asset.decimals)
|
||||
);
|
||||
|
||||
const threshold = await getThreshold(withdrawal.asset);
|
||||
|
||||
if (threshold && amount.isGreaterThan(threshold)) {
|
||||
const delaySecs = await getDelay();
|
||||
const completeTimestamp =
|
||||
new Date(withdrawal.createdTimestamp).getTime() + delaySecs * 1000;
|
||||
|
||||
if (Date.now() < completeTimestamp) {
|
||||
setState({
|
||||
status: ApprovalStatus.Delayed,
|
||||
threshold,
|
||||
completeTimestamp,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await client.query<Erc20Approval, Erc20ApprovalVariables>({
|
||||
query: ERC20_APPROVAL_QUERY,
|
||||
variables: { withdrawalId: withdrawal.id },
|
||||
});
|
||||
|
||||
const approval = res.data.erc20WithdrawalApproval;
|
||||
if (!approval) {
|
||||
setState({
|
||||
status: ApprovalStatus.Error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (approval.signatures.length < 3) {
|
||||
setState({
|
||||
status: ApprovalStatus.Error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
setState({ status: ApprovalStatus.Ready, dialogOpen: false });
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
setState({
|
||||
status: ApprovalStatus.Error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[getThreshold, getDelay, client, setState]
|
||||
);
|
||||
|
||||
return { verify, state, reset };
|
||||
};
|
@ -1,189 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import * as web3 from '@vegaprotocol/web3';
|
||||
import * as wallet from '@vegaprotocol/wallet';
|
||||
import type { WithdrawalFields } from './use-withdraw';
|
||||
import { useWithdraw } from './use-withdraw';
|
||||
import type { Erc20Approval } from './__generated__/Erc20Approval';
|
||||
|
||||
jest.mock('@vegaprotocol/web3', () => ({
|
||||
useBridgeContract: jest.fn().mockReturnValue({
|
||||
withdraw_asset: jest.fn(),
|
||||
isNewContract: true,
|
||||
}),
|
||||
useEthereumTransaction: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
useVegaWallet: jest.fn().mockReturnValue({ keypair: { pub: 'pubkey' } }),
|
||||
useVegaTransaction: jest.fn().mockReturnValue({
|
||||
transaction: {},
|
||||
send: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
function setup(mocks?: MockedResponse[], cancelled = false) {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useWithdraw(cancelled), { wrapper });
|
||||
}
|
||||
|
||||
const signature =
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909';
|
||||
const derivedWithdrawalId =
|
||||
'2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50';
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
let pubkey: string;
|
||||
let mockSend: jest.Mock;
|
||||
let mockPerform: jest.Mock;
|
||||
let mockEthReset: jest.Mock;
|
||||
let mockVegaReset: jest.Mock;
|
||||
let withdrawalInput: WithdrawalFields;
|
||||
let mockERC20Approval: MockedResponse<Erc20Approval>;
|
||||
|
||||
beforeEach(() => {
|
||||
pubkey = 'pubkey';
|
||||
mockSend = jest.fn().mockReturnValue(Promise.resolve({ signature }));
|
||||
mockPerform = jest.fn();
|
||||
mockEthReset = jest.fn();
|
||||
mockVegaReset = jest.fn();
|
||||
|
||||
jest.spyOn(web3, 'useEthereumTransaction').mockReturnValue({
|
||||
// @ts-ignore allow null transaction as its not used in hook logic
|
||||
transaction: null,
|
||||
perform: mockPerform,
|
||||
reset: mockEthReset,
|
||||
});
|
||||
jest
|
||||
.spyOn(wallet, 'useVegaWallet')
|
||||
// @ts-ignore only need to mock keypair
|
||||
.mockReturnValue({ keypair: { pub: pubkey } });
|
||||
jest.spyOn(wallet, 'useVegaTransaction').mockReturnValue({
|
||||
// @ts-ignore allow null transaction as its not used in hook logic
|
||||
transaction: null,
|
||||
send: mockSend,
|
||||
reset: mockVegaReset,
|
||||
});
|
||||
|
||||
withdrawalInput = {
|
||||
amount: '100',
|
||||
asset: 'asset-id',
|
||||
receiverAddress: 'receiver-address',
|
||||
};
|
||||
mockERC20Approval = {
|
||||
request: {
|
||||
query: ERC20_APPROVAL_QUERY,
|
||||
variables: { withdrawalId: derivedWithdrawalId },
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
erc20WithdrawalApproval: {
|
||||
__typename: 'Erc20WithdrawalApproval',
|
||||
assetSource: 'asset-source',
|
||||
amount: '100',
|
||||
nonce: '1',
|
||||
signatures: 'signatures',
|
||||
targetAddress: 'targetAddress',
|
||||
expiry: 'expiry',
|
||||
creation: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
delay: 5000,
|
||||
};
|
||||
});
|
||||
|
||||
it('Creates withdrawal and immediately submits Ethereum transaction', async () => {
|
||||
const { result } = setup([mockERC20Approval]);
|
||||
|
||||
await act(async () => {
|
||||
result.current.submit(withdrawalInput);
|
||||
});
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith({
|
||||
pubKey: pubkey,
|
||||
propagate: true,
|
||||
withdrawSubmission: {
|
||||
amount: withdrawalInput.amount,
|
||||
asset: withdrawalInput.asset,
|
||||
ext: {
|
||||
erc20: {
|
||||
receiverAddress: withdrawalInput.receiverAddress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.current.withdrawalId).toEqual(derivedWithdrawalId);
|
||||
|
||||
// Query 'poll' not returned yet and eth transaction shouldn't have
|
||||
// started
|
||||
expect(result.current.approval).toEqual(null);
|
||||
expect(mockPerform).not.toHaveBeenCalled();
|
||||
|
||||
// Advance query delay time so query result is returned and the
|
||||
// eth transaction should be called with the approval query result
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(mockERC20Approval.delay || 0);
|
||||
});
|
||||
|
||||
expect(result.current.approval).toEqual(
|
||||
// @ts-ignore MockedRespones types inteferring
|
||||
mockERC20Approval.result.data.erc20WithdrawalApproval
|
||||
);
|
||||
// @ts-ignore MockedRespones types inteferring
|
||||
const withdrawal = mockERC20Approval.result.data.erc20WithdrawalApproval;
|
||||
expect(mockPerform).toHaveBeenCalledWith(
|
||||
withdrawal.assetSource,
|
||||
withdrawal.amount,
|
||||
withdrawal.targetAddress,
|
||||
withdrawal.creation,
|
||||
withdrawal.nonce,
|
||||
withdrawal.signatures
|
||||
);
|
||||
});
|
||||
|
||||
it('Doesnt perform Ethereum tx if cancelled', async () => {
|
||||
const { result } = setup([mockERC20Approval], true);
|
||||
|
||||
await act(async () => {
|
||||
result.current.submit(withdrawalInput);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(mockERC20Approval.delay || 0);
|
||||
});
|
||||
|
||||
expect(result.current.approval).toEqual(
|
||||
// @ts-ignore MockedRespone types inteferring
|
||||
mockERC20Approval.result.data.erc20WithdrawalApproval
|
||||
);
|
||||
|
||||
// Approval set, but cancelled flag is set, so the Ethereum
|
||||
// TX should not be invoked
|
||||
expect(mockPerform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Reset will reset both transactions', async () => {
|
||||
const { result } = setup([mockERC20Approval]);
|
||||
|
||||
await act(async () => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(mockEthReset).toHaveBeenCalled();
|
||||
expect(mockVegaReset).toHaveBeenCalled();
|
||||
});
|
@ -1,115 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { determineId } from '@vegaprotocol/react-helpers';
|
||||
import { useBridgeContract, useEthereumTransaction } from '@vegaprotocol/web3';
|
||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import type {
|
||||
Erc20Approval,
|
||||
Erc20Approval_erc20WithdrawalApproval,
|
||||
Erc20ApprovalVariables,
|
||||
} from './__generated__/Erc20Approval';
|
||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
export interface WithdrawalFields {
|
||||
amount: string;
|
||||
asset: string;
|
||||
receiverAddress: string;
|
||||
}
|
||||
|
||||
export const useWithdraw = (cancelled: boolean) => {
|
||||
const [withdrawalId, setWithdrawalId] = useState<string | null>(null);
|
||||
const [approval, setApproval] =
|
||||
useState<Erc20Approval_erc20WithdrawalApproval | null>(null);
|
||||
|
||||
const contract = useBridgeContract();
|
||||
const { keypair } = useVegaWallet();
|
||||
const {
|
||||
transaction: vegaTx,
|
||||
send,
|
||||
reset: resetVegaTx,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const {
|
||||
transaction: ethTx,
|
||||
perform,
|
||||
reset: resetEthTx,
|
||||
} = useEthereumTransaction<CollateralBridge, 'withdraw_asset'>(
|
||||
contract,
|
||||
'withdraw_asset'
|
||||
);
|
||||
|
||||
const { data, stopPolling } = useQuery<Erc20Approval, Erc20ApprovalVariables>(
|
||||
ERC20_APPROVAL_QUERY,
|
||||
{
|
||||
variables: { withdrawalId: withdrawalId || '' },
|
||||
skip: !withdrawalId,
|
||||
pollInterval: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
const submit = useCallback(
|
||||
async (withdrawal: WithdrawalFields) => {
|
||||
if (!keypair) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await send({
|
||||
pubKey: keypair.pub,
|
||||
propagate: true,
|
||||
withdrawSubmission: {
|
||||
amount: withdrawal.amount,
|
||||
asset: withdrawal.asset,
|
||||
ext: {
|
||||
erc20: {
|
||||
receiverAddress: withdrawal.receiverAddress,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
setWithdrawalId(determineId(res.signature));
|
||||
}
|
||||
},
|
||||
[keypair, send]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data?.erc20WithdrawalApproval &&
|
||||
data.erc20WithdrawalApproval.signatures.length > 2
|
||||
) {
|
||||
stopPolling();
|
||||
setApproval(data.erc20WithdrawalApproval);
|
||||
}
|
||||
}, [data, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (approval && contract && !cancelled) {
|
||||
perform(
|
||||
approval.assetSource,
|
||||
approval.amount,
|
||||
approval.targetAddress,
|
||||
approval.creation,
|
||||
approval.nonce,
|
||||
approval.signatures
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [approval, contract]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetVegaTx();
|
||||
resetEthTx();
|
||||
}, [resetVegaTx, resetEthTx]);
|
||||
|
||||
return {
|
||||
submit,
|
||||
reset,
|
||||
approval,
|
||||
withdrawalId,
|
||||
vegaTx,
|
||||
ethTx,
|
||||
};
|
||||
};
|
58
libs/withdraws/src/lib/use-withdrawal-approval.ts
Normal file
58
libs/withdraws/src/lib/use-withdrawal-approval.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||
import type {
|
||||
Erc20Approval,
|
||||
Erc20ApprovalVariables,
|
||||
Erc20Approval_erc20WithdrawalApproval,
|
||||
} from './__generated__/Erc20Approval';
|
||||
|
||||
type WaitForApproval = (
|
||||
id: string
|
||||
) => Promise<Erc20Approval_erc20WithdrawalApproval>;
|
||||
|
||||
export const useWithdrawalApproval = () => {
|
||||
const client = useApolloClient();
|
||||
// eslint-disable-next-line
|
||||
const intervalRef = useRef<any>();
|
||||
|
||||
const waitForWithdrawalApproval = useCallback<WaitForApproval>(
|
||||
(id) => {
|
||||
return new Promise((resolve) => {
|
||||
intervalRef.current = setInterval(async () => {
|
||||
try {
|
||||
const res = await client.query<
|
||||
Erc20Approval,
|
||||
Erc20ApprovalVariables
|
||||
>({
|
||||
query: ERC20_APPROVAL_QUERY,
|
||||
variables: { withdrawalId: id },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (
|
||||
res.data.erc20WithdrawalApproval &&
|
||||
res.data.erc20WithdrawalApproval.signatures.length > 2
|
||||
) {
|
||||
clearInterval(intervalRef.current);
|
||||
resolve(res.data.erc20WithdrawalApproval);
|
||||
}
|
||||
} catch (err) {
|
||||
// no op as the query will error until the approval is created
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return waitForWithdrawalApproval;
|
||||
};
|
61
libs/withdraws/src/lib/use-withdrawal-event.ts
Normal file
61
libs/withdraws/src/lib/use-withdrawal-event.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { Subscription } from 'zen-observable-ts';
|
||||
import { WITHDRAWAL_BUS_EVENT_SUB } from './use-withdrawals';
|
||||
import type {
|
||||
WithdrawalEvent,
|
||||
WithdrawalEventVariables,
|
||||
WithdrawalEvent_busEvents_event_Withdrawal,
|
||||
} from './__generated__/WithdrawalEvent';
|
||||
|
||||
type WaitForWithdrawalEvent = (
|
||||
id: string,
|
||||
partyId: string
|
||||
) => Promise<WithdrawalEvent_busEvents_event_Withdrawal>;
|
||||
export const useWithdrawalEvent = () => {
|
||||
const client = useApolloClient();
|
||||
const subRef = useRef<Subscription | null>(null);
|
||||
|
||||
const waitForWithdrawalEvent = useCallback<WaitForWithdrawalEvent>(
|
||||
(id, partyId) => {
|
||||
return new Promise((resolve) => {
|
||||
subRef.current = client
|
||||
.subscribe<WithdrawalEvent, WithdrawalEventVariables>({
|
||||
query: WITHDRAWAL_BUS_EVENT_SUB,
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No types available for the subscription result
|
||||
const matchingWithdrawalEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Withdrawal') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.event.id === id;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingWithdrawalEvent &&
|
||||
matchingWithdrawalEvent.event.__typename === 'Withdrawal'
|
||||
) {
|
||||
resolve(matchingWithdrawalEvent.event);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
subRef.current?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return waitForWithdrawalEvent;
|
||||
};
|
@ -7,11 +7,11 @@ import type {
|
||||
} from './__generated__/WithdrawalEvent';
|
||||
import type {
|
||||
Withdrawals,
|
||||
Withdrawals_party_withdrawals,
|
||||
Withdrawals_party_withdrawalsConnection_edges_node,
|
||||
} from './__generated__/Withdrawals';
|
||||
|
||||
describe('updateQuery', () => {
|
||||
it('Updates existing withdrawals', () => {
|
||||
it('updates existing withdrawals', () => {
|
||||
const withdrawal = generateWithdrawal({
|
||||
id: '1',
|
||||
status: WithdrawalStatus.STATUS_OPEN,
|
||||
@ -27,7 +27,15 @@ describe('updateQuery', () => {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: 'party-id',
|
||||
withdrawals: [withdrawalUpdate],
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawalUpdate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -48,21 +56,45 @@ describe('updateQuery', () => {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: 'party-id',
|
||||
withdrawals: [withdrawalUpdate, withdrawal],
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawalUpdate,
|
||||
},
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawal,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Creates new party if not present', () => {
|
||||
it('creates new party if not present', () => {
|
||||
const partyId = 'party-id';
|
||||
const withdrawalUpdate = generateWithdrawal({
|
||||
id: '2',
|
||||
});
|
||||
const incoming = mockSub([withdrawalUpdate]);
|
||||
|
||||
expect(updateQuery({ party: null }, incoming)).toEqual({
|
||||
expect(
|
||||
updateQuery({ party: null }, { ...incoming, variables: { partyId } })
|
||||
).toEqual({
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
withdrawals: [withdrawalUpdate],
|
||||
id: partyId,
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawalUpdate,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -89,20 +121,42 @@ describe('updateQuery', () => {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: 'party-id',
|
||||
withdrawals: [withdrawalUpdate, withdrawalNew, withdrawal2],
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawalUpdate,
|
||||
},
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawalNew,
|
||||
},
|
||||
{
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: withdrawal2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockQuery = (
|
||||
withdrawals: Withdrawals_party_withdrawals[]
|
||||
withdrawals: Withdrawals_party_withdrawalsConnection_edges_node[]
|
||||
): Withdrawals => {
|
||||
return {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: 'party-id',
|
||||
withdrawals,
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges: withdrawals.map((w) => ({
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: w,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import compact from 'lodash/compact';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type {
|
||||
WithdrawalEvent,
|
||||
WithdrawalEventVariables,
|
||||
@ -12,6 +14,7 @@ import type {
|
||||
import type {
|
||||
Withdrawals,
|
||||
WithdrawalsVariables,
|
||||
Withdrawals_party_withdrawalsConnection_edges,
|
||||
} from './__generated__/Withdrawals';
|
||||
|
||||
const WITHDRAWAL_FRAGMENT = gql`
|
||||
@ -21,8 +24,14 @@ const WITHDRAWAL_FRAGMENT = gql`
|
||||
amount
|
||||
asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
createdTimestamp
|
||||
withdrawnTimestamp
|
||||
@ -41,8 +50,12 @@ export const WITHDRAWALS_QUERY = gql`
|
||||
query Withdrawals($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
withdrawals {
|
||||
...WithdrawalFields
|
||||
withdrawalsConnection {
|
||||
edges {
|
||||
node {
|
||||
...WithdrawalFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,7 +76,7 @@ export const WITHDRAWAL_BUS_EVENT_SUB = gql`
|
||||
|
||||
export const useWithdrawals = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const { subscribeToMore, ...queryResult } = useQuery<
|
||||
const { data, loading, error, subscribeToMore } = useQuery<
|
||||
Withdrawals,
|
||||
WithdrawalsVariables
|
||||
>(WITHDRAWALS_QUERY, {
|
||||
@ -85,19 +98,36 @@ export const useWithdrawals = () => {
|
||||
};
|
||||
}, [keypair?.pub, subscribeToMore]);
|
||||
|
||||
return queryResult;
|
||||
const withdrawals = useMemo(() => {
|
||||
if (!data?.party?.withdrawalsConnection?.edges) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderBy(
|
||||
compact(data.party.withdrawalsConnection.edges).map((edge) => edge.node),
|
||||
'createdTimestamp',
|
||||
'desc'
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
withdrawals,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateQuery: UpdateQueryFn<
|
||||
Withdrawals,
|
||||
WithdrawalEventVariables,
|
||||
WithdrawalEvent
|
||||
> = (prev, { subscriptionData }) => {
|
||||
> = (prev, { subscriptionData, variables }) => {
|
||||
if (!subscriptionData.data.busEvents?.length) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const curr = prev.party?.withdrawals || [];
|
||||
const curr = prev.party?.withdrawalsConnection.edges || [];
|
||||
const incoming = subscriptionData.data.busEvents
|
||||
.map((e) => {
|
||||
return {
|
||||
@ -105,21 +135,28 @@ export const updateQuery: UpdateQueryFn<
|
||||
pendingOnForeignChain: false,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
isWithdrawalEvent
|
||||
// Need this type cast here, TS can't infer that we've filtered any event types
|
||||
// that arent Withdrawals
|
||||
) as WithdrawalEvent_busEvents_event_Withdrawal[];
|
||||
.filter(isWithdrawalEvent)
|
||||
.map(
|
||||
(w) =>
|
||||
({
|
||||
__typename: 'WithdrawalEdge',
|
||||
node: w,
|
||||
} as Withdrawals_party_withdrawalsConnection_edges)
|
||||
);
|
||||
|
||||
const withdrawals = uniqBy([...incoming, ...curr], 'id');
|
||||
const edges = uniqBy([...incoming, ...curr], 'node.id');
|
||||
|
||||
// Write new party to cache if not present
|
||||
if (!prev.party) {
|
||||
return {
|
||||
...prev,
|
||||
party: {
|
||||
id: variables?.partyId,
|
||||
__typename: 'Party',
|
||||
withdrawals,
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges,
|
||||
},
|
||||
},
|
||||
} as Withdrawals;
|
||||
}
|
||||
@ -128,7 +165,10 @@ export const updateQuery: UpdateQueryFn<
|
||||
...prev,
|
||||
party: {
|
||||
...prev.party,
|
||||
withdrawals,
|
||||
withdrawalsConnection: {
|
||||
__typename: 'WithdrawalsConnection',
|
||||
edges,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,113 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import merge from 'lodash/merge';
|
||||
import {
|
||||
initialState as vegaTxInitialState,
|
||||
VegaTxStatus,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
EthTxStatus,
|
||||
initialState as ethTxInitialState,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { WithdrawDialogProps } from './withdraw-dialog';
|
||||
import { WithdrawDialog } from './withdraw-dialog';
|
||||
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
const approval: Erc20Approval_erc20WithdrawalApproval = {
|
||||
__typename: 'Erc20WithdrawalApproval',
|
||||
assetSource: 'asset-source',
|
||||
amount: '100',
|
||||
nonce: '1',
|
||||
signatures: 'signatures',
|
||||
targetAddress: 'target-address',
|
||||
expiry: 'expiry',
|
||||
creation: '1660158537747',
|
||||
};
|
||||
|
||||
const generateJsx = (override?: PartialDeep<WithdrawDialogProps>) => {
|
||||
const defaultProps = {
|
||||
vegaTx: vegaTxInitialState,
|
||||
ethTx: ethTxInitialState,
|
||||
approval: null,
|
||||
dialogOpen: true,
|
||||
onDialogChange: jest.fn(),
|
||||
};
|
||||
const props = merge(defaultProps, override);
|
||||
return <WithdrawDialog {...props} />;
|
||||
};
|
||||
|
||||
it('Dialog transaction states', () => {
|
||||
const { rerender } = render(
|
||||
generateJsx({
|
||||
vegaTx: {
|
||||
status: VegaTxStatus.Requested,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirm withdrawal')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
generateJsx({
|
||||
vegaTx: {
|
||||
status: VegaTxStatus.Pending,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('Withdrawal transaction pending')
|
||||
).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
generateJsx({
|
||||
vegaTx: {
|
||||
status: VegaTxStatus.Error,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText('Withdrawal transaction failed')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
generateJsx({
|
||||
approval,
|
||||
vegaTx: {
|
||||
status: VegaTxStatus.Pending,
|
||||
},
|
||||
ethTx: {
|
||||
status: EthTxStatus.Requested,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
||||
|
||||
const txHash = 'tx-hash';
|
||||
rerender(
|
||||
generateJsx({
|
||||
approval,
|
||||
ethTx: {
|
||||
status: EthTxStatus.Pending,
|
||||
txHash,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText('Ethereum transaction pending')).toBeInTheDocument();
|
||||
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(txHash)
|
||||
);
|
||||
|
||||
rerender(
|
||||
generateJsx({
|
||||
approval,
|
||||
ethTx: {
|
||||
status: EthTxStatus.Complete,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByText('Ethereum transaction complete')).toBeInTheDocument();
|
||||
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(txHash)
|
||||
);
|
||||
});
|
@ -1,182 +0,0 @@
|
||||
import { Link, Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { EthTxState } from '@vegaprotocol/web3';
|
||||
import { isEthereumError } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
||||
|
||||
export interface WithdrawDialogProps {
|
||||
vegaTx: VegaTxState;
|
||||
ethTx: EthTxState;
|
||||
approval: Erc20Approval_erc20WithdrawalApproval | null;
|
||||
dialogOpen: boolean;
|
||||
onDialogChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const WithdrawDialog = ({
|
||||
vegaTx,
|
||||
ethTx,
|
||||
approval,
|
||||
dialogOpen,
|
||||
onDialogChange,
|
||||
}: WithdrawDialogProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { intent, title, icon, children } = getProps(
|
||||
approval,
|
||||
vegaTx,
|
||||
ethTx,
|
||||
ETHERSCAN_URL
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onChange={onDialogChange}
|
||||
intent={intent}
|
||||
title={title}
|
||||
icon={icon}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
interface StepProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Step = ({ children }: StepProps) => {
|
||||
return (
|
||||
<p data-testid="dialog-text" className="flex justify-between">
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
interface DialogProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
children: ReactNode;
|
||||
intent?: Intent;
|
||||
}
|
||||
|
||||
const getProps = (
|
||||
approval: Erc20Approval_erc20WithdrawalApproval | null,
|
||||
vegaTx: VegaTxState,
|
||||
ethTx: EthTxState,
|
||||
ethUrl: string
|
||||
) => {
|
||||
const vegaTxPropsMap: Record<VegaTxStatus, DialogProps> = {
|
||||
[VegaTxStatus.Default]: {
|
||||
title: '',
|
||||
icon: null,
|
||||
intent: undefined,
|
||||
children: null,
|
||||
},
|
||||
[VegaTxStatus.Error]: {
|
||||
title: t('Withdrawal transaction failed'),
|
||||
icon: <Icon name="warning-sign" />,
|
||||
intent: Intent.Danger,
|
||||
children: (
|
||||
<Step>
|
||||
<p>{vegaTx.error}</p>
|
||||
</Step>
|
||||
),
|
||||
},
|
||||
[VegaTxStatus.Requested]: {
|
||||
title: t('Confirm withdrawal'),
|
||||
icon: <Icon name="hand-up" />,
|
||||
intent: Intent.Warning,
|
||||
children: <Step>Confirm withdrawal in Vega wallet</Step>,
|
||||
},
|
||||
[VegaTxStatus.Pending]: {
|
||||
title: t('Withdrawal transaction pending'),
|
||||
icon: <Loader size="small" />,
|
||||
intent: Intent.None,
|
||||
children: <Step>Awaiting transaction</Step>,
|
||||
},
|
||||
[VegaTxStatus.Complete]: {
|
||||
title: t('Withdrawal transaction complete'),
|
||||
icon: <Icon name="tick" />,
|
||||
intent: Intent.Success,
|
||||
children: <Step>Withdrawal created</Step>,
|
||||
},
|
||||
};
|
||||
|
||||
const completeProps = {
|
||||
title: t('Withdrawal complete'),
|
||||
icon: <Icon name="tick" />,
|
||||
intent: Intent.Success,
|
||||
children: (
|
||||
<Step>
|
||||
<span>{t('Ethereum transaction complete')}</span>
|
||||
<Link
|
||||
href={`${ethUrl}/tx/${ethTx.txHash}`}
|
||||
title={t('View transaction on Etherscan')}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
target="_blank"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</Link>
|
||||
</Step>
|
||||
),
|
||||
};
|
||||
|
||||
const ethTxPropsMap: Record<EthTxStatus, DialogProps> = {
|
||||
[EthTxStatus.Default]: {
|
||||
title: '',
|
||||
icon: null,
|
||||
intent: undefined,
|
||||
children: null,
|
||||
},
|
||||
[EthTxStatus.Error]: {
|
||||
title: t('Ethereum transaction failed'),
|
||||
icon: <Icon name="warning-sign" />,
|
||||
intent: Intent.Danger,
|
||||
children: (
|
||||
<Step>
|
||||
{isEthereumError(ethTx.error)
|
||||
? `Error: ${ethTx.error.reason}`
|
||||
: ethTx.error instanceof Error
|
||||
? t(`Error: ${ethTx.error.message}`)
|
||||
: t('Something went wrong')}
|
||||
</Step>
|
||||
),
|
||||
},
|
||||
[EthTxStatus.Requested]: {
|
||||
title: t('Confirm transaction'),
|
||||
icon: <Icon name="hand-up" />,
|
||||
intent: Intent.Warning,
|
||||
children: <Step>{t('Confirm transaction in wallet')}</Step>,
|
||||
},
|
||||
[EthTxStatus.Pending]: {
|
||||
title: t('Ethereum transaction pending'),
|
||||
icon: <Loader size="small" />,
|
||||
intent: Intent.None,
|
||||
children: (
|
||||
<Step>
|
||||
<span>
|
||||
{t(
|
||||
`Awaiting Ethereum transaction ${ethTx.confirmations}/1 confirmations...`
|
||||
)}
|
||||
</span>
|
||||
<Link
|
||||
href={`${ethUrl}/tx/${ethTx.txHash}`}
|
||||
title={t('View transaction on Etherscan')}
|
||||
className="text-vega-pink dark:text-vega-yellow"
|
||||
target="_blank"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</Link>
|
||||
</Step>
|
||||
),
|
||||
},
|
||||
[EthTxStatus.Complete]: completeProps,
|
||||
[EthTxStatus.Confirmed]: completeProps,
|
||||
};
|
||||
|
||||
return approval ? ethTxPropsMap[ethTx.status] : vegaTxPropsMap[vegaTx.status];
|
||||
};
|
90
libs/withdraws/src/lib/withdraw-form-container.tsx
Normal file
90
libs/withdraws/src/lib/withdraw-form-container.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useMemo } from 'react';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import { WithdrawManager } from './withdraw-manager';
|
||||
import type { WithdrawFormQuery } from './__generated__/WithdrawFormQuery';
|
||||
|
||||
export const ASSET_FRAGMENT = gql`
|
||||
fragment AssetFields on Asset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const WITHDRAW_FORM_QUERY = gql`
|
||||
${ASSET_FRAGMENT}
|
||||
query WithdrawFormQuery($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
withdrawals {
|
||||
id
|
||||
txHash
|
||||
}
|
||||
accounts {
|
||||
type
|
||||
balance
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface WithdrawFormContainerProps {
|
||||
partyId?: string;
|
||||
submit: (args: WithdrawalArgs) => void;
|
||||
}
|
||||
|
||||
export const WithdrawFormContainer = ({
|
||||
partyId,
|
||||
submit,
|
||||
}: WithdrawFormContainerProps) => {
|
||||
const { data, loading, error } = useQuery<WithdrawFormQuery>(
|
||||
WITHDRAW_FORM_QUERY,
|
||||
{
|
||||
variables: { partyId },
|
||||
}
|
||||
);
|
||||
|
||||
const assets = useMemo(() => {
|
||||
if (!data?.assetsConnection.edges) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return compact(data.assetsConnection.edges).map((e) => e.node);
|
||||
}, [data]);
|
||||
|
||||
if (loading || !data) {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{t('Something went wrong')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<WithdrawManager
|
||||
assets={assets}
|
||||
accounts={data.party?.accounts || []}
|
||||
submit={submit}
|
||||
/>
|
||||
);
|
||||
};
|
@ -26,13 +26,9 @@ beforeEach(() => {
|
||||
props = {
|
||||
assets,
|
||||
min: new BigNumber(0.00001),
|
||||
max: {
|
||||
balance: new BigNumber(100),
|
||||
threshold: new BigNumber(200),
|
||||
},
|
||||
limits: {
|
||||
max: new BigNumber(200),
|
||||
},
|
||||
balance: new BigNumber(100),
|
||||
threshold: new BigNumber(200),
|
||||
delay: 100,
|
||||
selectedAsset: undefined,
|
||||
onSelectAsset: jest.fn(),
|
||||
submitWithdraw: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
@ -112,6 +108,7 @@ describe('Withdrawal form', () => {
|
||||
asset: props.assets[0].id,
|
||||
amount: '4000000',
|
||||
receiverAddress: MOCK_ETH_ADDRESS,
|
||||
availableTimestamp: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -123,7 +120,7 @@ describe('Withdrawal form', () => {
|
||||
fireEvent.click(screen.getByText('Use maximum'));
|
||||
|
||||
expect(screen.getByLabelText('Amount')).toHaveValue(
|
||||
Number(props.max.balance.toFixed(asset.decimals))
|
||||
Number(props.balance.toFixed(asset.decimals))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
t,
|
||||
removeDecimal,
|
||||
required,
|
||||
maxSafe,
|
||||
isAssetTypeERC20,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
@ -15,12 +14,11 @@ import {
|
||||
InputError,
|
||||
Select,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Web3WalletInput } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import type { WithdrawalFields } from './use-withdraw';
|
||||
import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import { WithdrawLimits } from './withdraw-limits';
|
||||
|
||||
interface FormFields {
|
||||
@ -31,25 +29,22 @@ interface FormFields {
|
||||
|
||||
export interface WithdrawFormProps {
|
||||
assets: Asset[];
|
||||
max: {
|
||||
balance: BigNumber;
|
||||
threshold: BigNumber;
|
||||
};
|
||||
min: BigNumber;
|
||||
balance: BigNumber;
|
||||
selectedAsset?: Asset;
|
||||
limits: {
|
||||
max: BigNumber;
|
||||
} | null;
|
||||
threshold: BigNumber;
|
||||
delay: number | undefined;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
submitWithdraw: (withdrawal: WithdrawalFields) => void;
|
||||
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
||||
}
|
||||
|
||||
export const WithdrawForm = ({
|
||||
assets,
|
||||
max,
|
||||
balance,
|
||||
min,
|
||||
selectedAsset,
|
||||
limits,
|
||||
threshold,
|
||||
delay,
|
||||
onSelectAsset,
|
||||
submitWithdraw,
|
||||
}: WithdrawFormProps) => {
|
||||
@ -67,6 +62,9 @@ export const WithdrawForm = ({
|
||||
to: address,
|
||||
},
|
||||
});
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset) {
|
||||
throw new Error('Asset not selected');
|
||||
@ -75,103 +73,117 @@ export const WithdrawForm = ({
|
||||
asset: selectedAsset.id,
|
||||
amount: removeDecimal(fields.amount, selectedAsset.decimals),
|
||||
receiverAddress: fields.to,
|
||||
availableTimestamp:
|
||||
new BigNumber(fields.amount).isGreaterThan(threshold) && delay
|
||||
? Date.now() + delay * 1000
|
||||
: null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate={true}
|
||||
data-testid="withdraw-form"
|
||||
>
|
||||
<FormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{ validate: { required } }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
onSelectAsset(e.target.value);
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
value={selectedAsset?.id || ''}
|
||||
id="asset"
|
||||
>
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<>
|
||||
<div className="mb-4 text-sm">
|
||||
<p>{t('There are two steps required to make a withdrawal')}</p>
|
||||
<ol className="list-disc pl-4">
|
||||
<li>{t('Step 1 - Release funds from Vega')}</li>
|
||||
<li>{t('Step 2 - Transfer funds to your Ethereum wallet')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate={true}
|
||||
data-testid="withdraw-form"
|
||||
>
|
||||
<FormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{ validate: { required } }}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
onSelectAsset(e.target.value);
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
value={selectedAsset?.id || ''}
|
||||
id="asset"
|
||||
name="asset"
|
||||
>
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<InputError intent="danger">{errors.asset.message}</InputError>
|
||||
)}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<InputError intent="danger">{errors.asset.message}</InputError>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t('To (Ethereum address)')}
|
||||
labelFor="ethereum-address"
|
||||
>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
{...register('to', { validate: { required, ethereumAddress } })}
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger">{errors.to.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && threshold && (
|
||||
<div className="mb-4">
|
||||
<WithdrawLimits
|
||||
amount={amount}
|
||||
threshold={threshold}
|
||||
delay={delay}
|
||||
balance={balance}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label={t('To (Ethereum address)')} labelFor="ethereum-address">
|
||||
<Web3WalletInput
|
||||
inputProps={{
|
||||
id: 'ethereum-address',
|
||||
...register('to', { validate: { required, ethereumAddress } }),
|
||||
}}
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger">{errors.to.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && limits && (
|
||||
<div className="mb-6">
|
||||
<WithdrawLimits limits={limits} balance={max.balance} />
|
||||
</div>
|
||||
)}
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max.balance)) {
|
||||
return t('Insufficient amount in account');
|
||||
} else if (value.isGreaterThan(max.threshold)) {
|
||||
return t('Amount is above temporary withdrawal limit');
|
||||
}
|
||||
return maxSafe(BigNumber.minimum(max.balance, max.threshold))(
|
||||
v
|
||||
);
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(balance)) {
|
||||
return t('Insufficient amount in account');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
minSafe: (value) => minSafe(min)(value),
|
||||
},
|
||||
minSafe: (value) => minSafe(min)(value),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" className="mt-4">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{selectedAsset && (
|
||||
<UseButton
|
||||
data-testid="use-maximum"
|
||||
onClick={() => {
|
||||
setValue('amount', max.balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button variant="primary" data-testid="submit-withdrawal" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger">{errors.amount.message}</InputError>
|
||||
)}
|
||||
{selectedAsset && (
|
||||
<UseButton
|
||||
data-testid="use-maximum"
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<Button data-testid="submit-withdrawal" type="submit" variant="primary">
|
||||
Release funds
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,24 +1,35 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
interface WithdrawLimitsProps {
|
||||
limits: {
|
||||
max: BigNumber;
|
||||
};
|
||||
amount: string;
|
||||
threshold: BigNumber;
|
||||
balance: BigNumber;
|
||||
delay: number | undefined;
|
||||
}
|
||||
|
||||
export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
|
||||
let maxLimit = '';
|
||||
export const WithdrawLimits = ({
|
||||
amount,
|
||||
threshold,
|
||||
balance,
|
||||
delay,
|
||||
}: WithdrawLimitsProps) => {
|
||||
let text = '';
|
||||
|
||||
if (limits.max.isEqualTo(Infinity)) {
|
||||
maxLimit = t('No limit');
|
||||
} else if (limits.max.isGreaterThan(1_000_000)) {
|
||||
maxLimit = t('1m+');
|
||||
if (threshold.isEqualTo(Infinity)) {
|
||||
text = t('No limit');
|
||||
} else if (threshold.isGreaterThan(1_000_000)) {
|
||||
text = t('1m+');
|
||||
} else {
|
||||
maxLimit = limits.max.toString();
|
||||
text = threshold.toString();
|
||||
}
|
||||
|
||||
const delayTime =
|
||||
new BigNumber(amount).isGreaterThan(threshold) && delay
|
||||
? formatDistanceToNow(Date.now() + delay * 1000)
|
||||
: t('None');
|
||||
|
||||
return (
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
@ -27,8 +38,14 @@ export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
|
||||
<td className="text-right">{balance.toString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Maximum withdrawal')}</th>
|
||||
<td className="text-right">{maxLimit}</td>
|
||||
<th className="text-left font-normal">
|
||||
{t('Delayed withdrawal threshold')}
|
||||
</th>
|
||||
<td className="text-right">{text}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="text-left font-normal">{t('Delay time')}</th>
|
||||
<td className="text-right">{delayTime}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { generateAccount, generateAsset } from './test-helpers';
|
||||
import type { WithdrawManagerProps } from './withdraw-manager';
|
||||
import { WithdrawManager } from './withdraw-manager';
|
||||
import * as withdrawHook from './use-withdraw';
|
||||
import { initialState as vegaTxInitialState } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
EthereumError,
|
||||
EthTxStatus,
|
||||
initialState as ethTxInitialState,
|
||||
} from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
@ -17,72 +11,46 @@ jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({ account: ethereumAddress }),
|
||||
}));
|
||||
|
||||
jest.mock('./use-get-withdraw-limits', () => ({
|
||||
useGetWithdrawLimits: () => {
|
||||
return { max: new BigNumber(1000000) };
|
||||
jest.mock('./use-get-withdraw-threshold', () => ({
|
||||
useGetWithdrawThreshold: () => {
|
||||
return () => Promise.resolve(new BigNumber(100));
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./use-get-withdraw-delay', () => ({
|
||||
useGetWithdrawDelay: () => {
|
||||
return () => Promise.resolve(10000);
|
||||
},
|
||||
}));
|
||||
|
||||
let props: WithdrawManagerProps;
|
||||
let useWithdrawValue: ReturnType<typeof withdrawHook.useWithdraw>;
|
||||
let useWithdraw: jest.SpyInstance;
|
||||
let mockSubmit: jest.Mock;
|
||||
let mockReset: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
assets: [generateAsset()],
|
||||
accounts: [generateAccount()],
|
||||
initialAssetId: undefined,
|
||||
submit: jest.fn(),
|
||||
};
|
||||
mockSubmit = jest.fn();
|
||||
mockReset = jest.fn();
|
||||
useWithdrawValue = {
|
||||
ethTx: ethTxInitialState,
|
||||
vegaTx: vegaTxInitialState,
|
||||
approval: null,
|
||||
withdrawalId: null,
|
||||
submit: mockSubmit,
|
||||
reset: mockReset,
|
||||
};
|
||||
useWithdraw = jest
|
||||
.spyOn(withdrawHook, 'useWithdraw')
|
||||
.mockReturnValue(useWithdrawValue);
|
||||
});
|
||||
|
||||
const generateJsx = (props: WithdrawManagerProps) => (
|
||||
<WithdrawManager {...props} />
|
||||
);
|
||||
|
||||
it('Valid form submission opens transaction dialog', async () => {
|
||||
it('calls submit if valid form submission', async () => {
|
||||
render(generateJsx(props));
|
||||
submitValid();
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
expect(mockReset).toHaveBeenCalled();
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
await act(async () => {
|
||||
await submitValid();
|
||||
});
|
||||
expect(props.submit).toHaveBeenCalledWith({
|
||||
amount: '1000',
|
||||
asset: props.assets[0].id,
|
||||
receiverAddress: ethereumAddress,
|
||||
availableTimestamp: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('Expected Ethereum error closes the dialog', async () => {
|
||||
const { rerender } = render(generateJsx(props));
|
||||
submitValid();
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
useWithdraw.mockReturnValue({
|
||||
...useWithdrawValue,
|
||||
ethTx: {
|
||||
...useWithdrawValue.ethTx,
|
||||
status: EthTxStatus.Error,
|
||||
error: new EthereumError('User rejected transaction', 4001, 'reason'),
|
||||
},
|
||||
});
|
||||
rerender(generateJsx(props));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Correct min max values provided to form', async () => {
|
||||
it('validates correctly', async () => {
|
||||
render(generateJsx(props));
|
||||
|
||||
// Set other fields to be valid
|
||||
@ -99,7 +67,7 @@ it('Correct min max values provided to form', async () => {
|
||||
});
|
||||
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
||||
expect(mockSubmit).not.toBeCalled();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00001' },
|
||||
@ -113,19 +81,14 @@ it('Correct min max values provided to form', async () => {
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in account')
|
||||
).toBeInTheDocument();
|
||||
expect(mockSubmit).not.toBeCalled();
|
||||
expect(props.submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('Initial asset id can preselect asset', async () => {
|
||||
const asset = props.assets[0];
|
||||
render(generateJsx({ ...props, initialAssetId: asset.id }));
|
||||
expect(screen.getByLabelText('Asset')).toHaveValue(asset.id);
|
||||
});
|
||||
|
||||
const submitValid = () => {
|
||||
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||
target: { value: props.assets[0].id },
|
||||
});
|
||||
const submitValid = async () => {
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Asset'),
|
||||
props.assets[0].id
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
|
@ -1,118 +1,78 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { WithdrawForm } from './withdraw-form';
|
||||
import type { WithdrawalFields } from './use-withdraw';
|
||||
import { useWithdraw } from './use-withdraw';
|
||||
import { WithdrawDialog } from './withdraw-dialog';
|
||||
import { isExpectedEthereumError, EthTxStatus } from '@vegaprotocol/web3';
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Account } from './types';
|
||||
import { useGetWithdrawLimits } from './use-get-withdraw-limits';
|
||||
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
|
||||
import { captureException } from '@sentry/react';
|
||||
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
|
||||
import { useWithdrawStore } from './withdraw-store';
|
||||
import type { AssetFields } from './__generated__/AssetFields';
|
||||
|
||||
export interface WithdrawManagerProps {
|
||||
assets: Asset[];
|
||||
assets: AssetFields[];
|
||||
accounts: Account[];
|
||||
initialAssetId?: string;
|
||||
submit: (args: WithdrawalArgs) => void;
|
||||
}
|
||||
|
||||
export const WithdrawManager = ({
|
||||
assets,
|
||||
accounts,
|
||||
initialAssetId,
|
||||
submit,
|
||||
}: WithdrawManagerProps) => {
|
||||
const dialogDismissed = useRef(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
|
||||
const { asset, balance, min, threshold, delay, update } = useWithdrawStore();
|
||||
const getThreshold = useGetWithdrawThreshold();
|
||||
const getDelay = useGetWithdrawDelay();
|
||||
|
||||
const { ethTx, vegaTx, approval, submit, reset } = useWithdraw(
|
||||
dialogDismissed.current
|
||||
);
|
||||
// Everytime an asset is selected we need to find the corresponding
|
||||
// account, balance, min viable amount and delay threshold
|
||||
const handleSelectAsset = useCallback(
|
||||
async (id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
const account = accounts.find(
|
||||
(a) =>
|
||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL &&
|
||||
a.asset.id === asset?.id
|
||||
);
|
||||
const balance =
|
||||
account && asset
|
||||
? new BigNumber(addDecimal(account.balance, asset.decimals))
|
||||
: new BigNumber(0);
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
// Find the asset object from the select box
|
||||
const asset = useMemo(() => {
|
||||
return assets?.find((a) => a.id === assetId);
|
||||
}, [assets, assetId]);
|
||||
// Query collateral bridge for threshold for selected asset
|
||||
// and subsequent delay if withdrawal amount is larger than it
|
||||
let threshold;
|
||||
let delay;
|
||||
|
||||
const account = useMemo(() => {
|
||||
return accounts.find(
|
||||
(a) =>
|
||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL && a.asset.id === asset?.id
|
||||
);
|
||||
}, [asset, accounts]);
|
||||
try {
|
||||
const result = await Promise.all([getThreshold(asset), getDelay()]);
|
||||
threshold = result[0];
|
||||
delay = result[1];
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
}
|
||||
|
||||
const limits = useGetWithdrawLimits(asset);
|
||||
|
||||
const max = useMemo(() => {
|
||||
if (!asset) {
|
||||
return {
|
||||
balance: new BigNumber(0),
|
||||
threshold: new BigNumber(0),
|
||||
};
|
||||
}
|
||||
|
||||
const balance = account
|
||||
? new BigNumber(addDecimal(account.balance, asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
return {
|
||||
balance,
|
||||
threshold: limits ? limits.max : new BigNumber(Infinity),
|
||||
};
|
||||
}, [asset, account, limits]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
return asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
}, [asset]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(args: WithdrawalFields) => {
|
||||
reset();
|
||||
setDialogOpen(true);
|
||||
submit(args);
|
||||
dialogDismissed.current = false;
|
||||
update({ asset, balance, min, threshold, delay });
|
||||
},
|
||||
[submit, reset]
|
||||
[accounts, assets, update, getThreshold, getDelay]
|
||||
);
|
||||
|
||||
// Close dialog if error is due to user rejecting the tx
|
||||
useEffect(() => {
|
||||
if (
|
||||
ethTx.status === EthTxStatus.Error &&
|
||||
isExpectedEthereumError(ethTx.error)
|
||||
) {
|
||||
setDialogOpen(false);
|
||||
}
|
||||
}, [ethTx.status, ethTx.error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WithdrawForm
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={(id) => setAssetId(id)}
|
||||
assets={sortBy(assets, 'name')}
|
||||
max={max}
|
||||
min={min}
|
||||
submitWithdraw={handleSubmit}
|
||||
limits={limits}
|
||||
/>
|
||||
<WithdrawDialog
|
||||
vegaTx={vegaTx}
|
||||
ethTx={ethTx}
|
||||
approval={approval}
|
||||
dialogOpen={dialogOpen}
|
||||
onDialogChange={(isOpen) => {
|
||||
setDialogOpen(isOpen);
|
||||
|
||||
if (!isOpen) {
|
||||
dialogDismissed.current = true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<WithdrawForm
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={handleSelectAsset}
|
||||
assets={sortBy(assets, 'name')}
|
||||
balance={balance}
|
||||
min={min}
|
||||
submitWithdraw={submit}
|
||||
threshold={threshold}
|
||||
delay={delay}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
24
libs/withdraws/src/lib/withdraw-store.ts
Normal file
24
libs/withdraws/src/lib/withdraw-store.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { Asset } from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { SetState } from 'zustand';
|
||||
import create from 'zustand';
|
||||
|
||||
interface WithdrawStore {
|
||||
asset: Asset | undefined;
|
||||
balance: BigNumber;
|
||||
min: BigNumber;
|
||||
threshold: BigNumber;
|
||||
delay: number | undefined;
|
||||
update: (state: Partial<WithdrawStore>) => void;
|
||||
}
|
||||
|
||||
export const useWithdrawStore = create((set: SetState<WithdrawStore>) => ({
|
||||
asset: undefined,
|
||||
balance: new BigNumber(0),
|
||||
min: new BigNumber(0),
|
||||
threshold: new BigNumber(0),
|
||||
delay: undefined,
|
||||
update: (updatedState) => {
|
||||
set(updatedState);
|
||||
},
|
||||
}));
|
49
libs/withdraws/src/lib/withdrawal-dialogs.tsx
Normal file
49
libs/withdraws/src/lib/withdrawal-dialogs.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCompleteWithdraw } from './use-complete-withdraw';
|
||||
import { useCreateWithdraw } from './use-create-withdraw';
|
||||
import { WithdrawFormContainer } from './withdraw-form-container';
|
||||
import { WithdrawalFeedback } from './withdrawal-feedback';
|
||||
|
||||
export const WithdrawalDialogs = ({
|
||||
withdrawDialog,
|
||||
setWithdrawDialog,
|
||||
}: {
|
||||
withdrawDialog: boolean;
|
||||
setWithdrawDialog: (open: boolean) => void;
|
||||
}) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
const createWithdraw = useCreateWithdraw();
|
||||
const completeWithdraw = useCompleteWithdraw();
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
title={t('Withdraw')}
|
||||
open={withdrawDialog}
|
||||
onChange={(isOpen) => setWithdrawDialog(isOpen)}
|
||||
size="small"
|
||||
>
|
||||
<WithdrawFormContainer
|
||||
partyId={keypair?.pub}
|
||||
submit={(args) => {
|
||||
setWithdrawDialog(false);
|
||||
createWithdraw.submit(args);
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
<createWithdraw.Dialog>
|
||||
<WithdrawalFeedback
|
||||
withdrawal={createWithdraw.withdrawal}
|
||||
transaction={createWithdraw.transaction}
|
||||
availableTimestamp={createWithdraw.availableTimestamp}
|
||||
submitWithdraw={(id) => {
|
||||
createWithdraw.reset();
|
||||
completeWithdraw.submit(id);
|
||||
}}
|
||||
/>
|
||||
</createWithdraw.Dialog>
|
||||
<completeWithdraw.Dialog />
|
||||
</>
|
||||
);
|
||||
};
|
96
libs/withdraws/src/lib/withdrawal-feedback.tsx
Normal file
96
libs/withdraws/src/lib/withdrawal-feedback.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
t,
|
||||
truncateByChars,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
|
||||
|
||||
export const WithdrawalFeedback = ({
|
||||
transaction,
|
||||
withdrawal,
|
||||
submitWithdraw,
|
||||
availableTimestamp,
|
||||
}: {
|
||||
transaction: VegaTxState;
|
||||
withdrawal: WithdrawalFields | null;
|
||||
submitWithdraw: (withdrawalId: string) => void;
|
||||
availableTimestamp: number | null;
|
||||
}) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const isAvailable =
|
||||
availableTimestamp === null || Date.now() > availableTimestamp;
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
{t('Your funds have been unlocked for withdrawal')} -{' '}
|
||||
<a
|
||||
className="underline"
|
||||
data-testid="tx-block-explorer"
|
||||
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View in block explorer')}
|
||||
</a>
|
||||
</p>
|
||||
{withdrawal && (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('Asset')}</span>
|
||||
<span>{withdrawal.asset.symbol}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
<span>{t('Amount')}</span>
|
||||
<span>
|
||||
{addDecimalsFormatNumber(
|
||||
withdrawal.amount,
|
||||
withdrawal.asset.decimals
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
{withdrawal.details && (
|
||||
<KeyValueTableRow>
|
||||
<span>{t('Recipient')}</span>
|
||||
<a
|
||||
target="_blank"
|
||||
href={`${VEGA_EXPLORER_URL}/address/${withdrawal.details.receiverAddress}`}
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{truncateByChars(withdrawal.details.receiverAddress)}
|
||||
</a>
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
</KeyValueTable>
|
||||
)}
|
||||
{isAvailable ? (
|
||||
<Button
|
||||
disabled={withdrawal === null ? true : false}
|
||||
onClick={() => {
|
||||
if (withdrawal) {
|
||||
submitWithdraw(withdrawal.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('Withdraw funds')}
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-danger">
|
||||
{t(
|
||||
`Available to withdraw in ${formatDistanceToNow(
|
||||
availableTimestamp
|
||||
)}`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -4,7 +4,7 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||
import { WithdrawalStatus, WithdrawalStatusMapping } from '@vegaprotocol/types';
|
||||
import { generateWithdrawal } from './test-helpers';
|
||||
import type {
|
||||
StatusCellProps,
|
||||
@ -12,7 +12,7 @@ import type {
|
||||
} from './withdrawals-table';
|
||||
import { StatusCell } from './withdrawals-table';
|
||||
import { WithdrawalsTable } from './withdrawals-table';
|
||||
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||
import type { Withdrawals_party_withdrawalsConnection_edges_node } from './__generated__/Withdrawals';
|
||||
|
||||
jest.mock('@web3-react/core', () => ({
|
||||
useWeb3React: () => ({ provider: undefined }),
|
||||
@ -24,38 +24,66 @@ const generateJsx = (props: WithdrawalsTableProps) => (
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
it('Renders the correct columns', async () => {
|
||||
const withdrawal = generateWithdrawal();
|
||||
await act(async () => {
|
||||
render(generateJsx({ withdrawals: [withdrawal] }));
|
||||
describe('renders the correct columns', () => {
|
||||
it('incomplete withdrawal', async () => {
|
||||
const withdrawal = generateWithdrawal();
|
||||
await act(async () => {
|
||||
render(generateJsx({ withdrawals: [withdrawal] }));
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(6);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||
'Asset',
|
||||
'Amount',
|
||||
'Recipient',
|
||||
'Created at',
|
||||
'TX hash',
|
||||
'Status',
|
||||
]);
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'asset-symbol',
|
||||
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
||||
'123456\u2026123456',
|
||||
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||
'-',
|
||||
WithdrawalStatusMapping[withdrawal.status],
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(5);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||
'Asset',
|
||||
'Amount',
|
||||
'Recipient',
|
||||
'Created at',
|
||||
'Status',
|
||||
]);
|
||||
it('completed withdrawal', async () => {
|
||||
const withdrawal = generateWithdrawal({
|
||||
txHash: '0x1234567891011121314',
|
||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'asset-symbol',
|
||||
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
||||
'123456\u2026123456',
|
||||
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||
withdrawal.status,
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
await act(async () => {
|
||||
render(generateJsx({ withdrawals: [withdrawal] }));
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
'asset-symbol',
|
||||
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
||||
'123456…123456',
|
||||
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||
'0x1234…121314',
|
||||
WithdrawalStatusMapping[withdrawal.status],
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('StatusCell', () => {
|
||||
let props: StatusCellProps;
|
||||
let withdrawal: Withdrawals_party_withdrawals;
|
||||
let withdrawal: Withdrawals_party_withdrawalsConnection_edges_node;
|
||||
let mockComplete: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -76,9 +104,7 @@ describe('StatusCell', () => {
|
||||
render(<StatusCell {...props} />);
|
||||
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
screen.getByText('Click to complete', { selector: 'button' })
|
||||
);
|
||||
fireEvent.click(screen.getByText('Complete', { selector: 'button' }));
|
||||
expect(mockComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -102,18 +128,5 @@ describe('StatusCell', () => {
|
||||
render(<StatusCell {...props} />);
|
||||
|
||||
expect(screen.getByText('Finalized')).toBeInTheDocument();
|
||||
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(props.data.txHash)
|
||||
);
|
||||
});
|
||||
|
||||
it('Fallback', () => {
|
||||
props.value = WithdrawalStatus.STATUS_REJECTED;
|
||||
props.data.pendingOnForeignChain = false;
|
||||
props.data.txHash = '0x123';
|
||||
render(<StatusCell {...props} />);
|
||||
|
||||
expect(screen.getByText('STATUS_REJECTED')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,3 @@
|
||||
import type {
|
||||
ICellRendererParams,
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import {
|
||||
getDateTimeFormat,
|
||||
@ -9,19 +5,40 @@ import {
|
||||
truncateByChars,
|
||||
addDecimalsFormatNumber,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Dialog,
|
||||
Link,
|
||||
AgGridDynamic as AgGrid,
|
||||
Intent,
|
||||
Loader,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useCompleteWithdraw } from './use-complete-withdraw';
|
||||
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||
import type { WithdrawalFields } from './__generated__/WithdrawalFields';
|
||||
import type { VerifyState } from './use-verify-withdrawal';
|
||||
import { ApprovalStatus, useVerifyWithdrawal } from './use-verify-withdrawal';
|
||||
|
||||
export interface WithdrawalsTableProps {
|
||||
withdrawals: Withdrawals_party_withdrawals[];
|
||||
withdrawals: WithdrawalFields[];
|
||||
}
|
||||
|
||||
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { submit, Dialog } = useCompleteWithdraw();
|
||||
const {
|
||||
submit,
|
||||
reset: resetTx,
|
||||
Dialog: EthereumTransactionDialog,
|
||||
} = useCompleteWithdraw();
|
||||
const {
|
||||
verify,
|
||||
state: verifyState,
|
||||
reset: resetVerification,
|
||||
} = useVerifyWithdrawal();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -35,51 +52,105 @@ export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
||||
>
|
||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||
<AgGridColumn
|
||||
headerName="Amount"
|
||||
headerName={t('Amount')}
|
||||
field="amount"
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<WithdrawalFields, 'amount'>) => {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Recipient"
|
||||
headerName={t('Recipient')}
|
||||
field="details.receiverAddress"
|
||||
cellRenderer="RecipientCell"
|
||||
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
|
||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFields,
|
||||
'details.receiverAddress'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
return truncateByChars(value);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Created at"
|
||||
headerName={t('Created at')}
|
||||
field="createdTimestamp"
|
||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
WithdrawalFields,
|
||||
'createdTimestamp'
|
||||
>) => {
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName="Status"
|
||||
headerName={t('TX hash')}
|
||||
field="txHash"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<WithdrawalFields, 'txHash'>) => {
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(value)}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="status"
|
||||
cellRenderer="StatusCell"
|
||||
cellRendererParams={{ complete: submit, ethUrl: ETHERSCAN_URL }}
|
||||
cellRendererParams={{
|
||||
complete: async (withdrawal: WithdrawalFields) => {
|
||||
const verified = await verify(withdrawal);
|
||||
|
||||
if (!verified) {
|
||||
return;
|
||||
}
|
||||
|
||||
submit(withdrawal.id);
|
||||
},
|
||||
ethUrl: ETHERSCAN_URL,
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
<Dialog />
|
||||
<Dialog
|
||||
title={t('Withdrawal verification')}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
resetTx();
|
||||
resetVerification();
|
||||
}
|
||||
}}
|
||||
open={verifyState.dialogOpen}
|
||||
size="small"
|
||||
{...getVerifyDialogProps(verifyState.status)}
|
||||
>
|
||||
<VerificationStatus state={verifyState} />
|
||||
</Dialog>
|
||||
<EthereumTransactionDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface StatusCellProps extends ICellRendererParams {
|
||||
export interface StatusCellProps
|
||||
extends VegaICellRendererParams<WithdrawalFields, 'status'> {
|
||||
ethUrl: string;
|
||||
complete: (withdrawalId: string) => void;
|
||||
complete: (withdrawal: WithdrawalFields) => void;
|
||||
}
|
||||
|
||||
export const StatusCell = ({
|
||||
ethUrl,
|
||||
value,
|
||||
data,
|
||||
complete,
|
||||
}: StatusCellProps) => {
|
||||
export const StatusCell = ({ ethUrl, data, complete }: StatusCellProps) => {
|
||||
if (data.pendingOnForeignChain) {
|
||||
return (
|
||||
<div className="flex justify-between gap-8">
|
||||
@ -98,37 +169,22 @@ export const StatusCell = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (value === WithdrawalStatus.STATUS_FINALIZED) {
|
||||
if (!data.txHash) {
|
||||
return (
|
||||
<div className="flex justify-between gap-8">
|
||||
{data.txHash ? (
|
||||
<>
|
||||
{t('Finalized')}
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ethUrl}/tx/${data.txHash}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Open')}
|
||||
<button className="underline" onClick={() => complete(data.id)}>
|
||||
{t('Click to complete')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{t('Open')}
|
||||
<button className="underline" onClick={() => complete(data)}>
|
||||
{t('Complete')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
return <span>{t('Finalized')}</span>;
|
||||
};
|
||||
|
||||
export interface RecipientCellProps extends ICellRendererParams {
|
||||
export interface RecipientCellProps
|
||||
extends VegaICellRendererParams<WithdrawalFields, 'details.receiverAddress'> {
|
||||
ethUrl: string;
|
||||
}
|
||||
|
||||
@ -148,3 +204,48 @@ const RecipientCell = ({
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const getVerifyDialogProps = (status: ApprovalStatus) => {
|
||||
if (status === ApprovalStatus.Error) {
|
||||
return {
|
||||
intent: Intent.Danger,
|
||||
icon: <Icon name="warning-sign" />,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === ApprovalStatus.Pending) {
|
||||
return { intent: Intent.None, icon: <Loader size="small" /> };
|
||||
}
|
||||
|
||||
if (status === ApprovalStatus.Delayed) {
|
||||
return { intent: Intent.Warning, icon: <Icon name="time" /> };
|
||||
}
|
||||
|
||||
return { intent: Intent.None };
|
||||
};
|
||||
|
||||
const VerificationStatus = ({ state }: { state: VerifyState }) => {
|
||||
if (state.status === ApprovalStatus.Error) {
|
||||
return <p>{t('Something went wrong')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Pending) {
|
||||
return <p>{t('Verifying...')}</p>;
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
|
||||
const formattedTime = getDateTimeFormat().format(
|
||||
new Date(state.completeTimestamp)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t("The amount you're withdrawing has triggered a time delay")}
|
||||
</p>
|
||||
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -15,7 +15,6 @@
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"src/lib/withdraw-manager.foo.tsx",
|
||||
"jest.config.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user