* 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,
|
fieldErrors: errors,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||||
useOrderSubmit();
|
|
||||||
|
|
||||||
const onSizeChange = (value: number[]) => {
|
const onSizeChange = (value: number[]) => {
|
||||||
const newVal = new BigNumber(value[0])
|
const newVal = new BigNumber(value[0])
|
||||||
@ -249,13 +248,13 @@ export const DealTicketSteps = ({
|
|||||||
notionalSize={notionalSize || emptyString}
|
notionalSize={notionalSize || emptyString}
|
||||||
fees={fees || emptyString}
|
fees={fees || emptyString}
|
||||||
/>
|
/>
|
||||||
<TransactionDialog
|
<Dialog
|
||||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||||
>
|
>
|
||||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||||
</TransactionDialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
@ -3,7 +3,7 @@ const navHome = '[href="/"]';
|
|||||||
const navVesting = '[href="/vesting"]';
|
const navVesting = '[href="/vesting"]';
|
||||||
const navStaking = '[href="/staking"]';
|
const navStaking = '[href="/staking"]';
|
||||||
const navRewards = '[href="/rewards"]';
|
const navRewards = '[href="/rewards"]';
|
||||||
const navWithdraw = '[href="/withdraw"]';
|
const navWithdraw = '[href="/withdrawals"]';
|
||||||
const navGovernance = '[href="/governance"]';
|
const navGovernance = '[href="/governance"]';
|
||||||
|
|
||||||
const tokenDetailsTable = '.token-details';
|
const tokenDetailsTable = '.token-details';
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
|
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
|
||||||
const warning = '[data-testid="callout"]';
|
|
||||||
|
|
||||||
context('Withdraw Page - verify elements on page', function () {
|
context('Withdraw Page - verify elements on page', function () {
|
||||||
before('navigate to withdraw page', function () {
|
before('navigate to withdrawals page', function () {
|
||||||
cy.visit('/').navigate_to('withdraw');
|
cy.visit('/').navigate_to('withdrawals');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with wallets disconnected', function () {
|
describe('with wallets disconnected', function () {
|
||||||
it('should have withdraw tab highlighted', 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 () {
|
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 () {
|
it('should have connect Vega wallet button', function () {
|
||||||
@ -20,9 +19,5 @@ context('Withdraw Page - verify elements on page', function () {
|
|||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.and('have.text', 'Connect Vega wallet');
|
.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"]',
|
vesting: '[href="/vesting"]',
|
||||||
staking: '[href="/staking"]',
|
staking: '[href="/staking"]',
|
||||||
rewards: '[href="/rewards"]',
|
rewards: '[href="/rewards"]',
|
||||||
withdraw: '[href="/withdraw"]',
|
withdrawals: '[href="/withdrawals"]',
|
||||||
governance: '[href="/governance"]',
|
governance: '[href="/governance"]',
|
||||||
pageSpinner: '[data-testid="splash-loader"]',
|
pageSpinner: '[data-testid="splash-loader"]',
|
||||||
};
|
};
|
||||||
|
@ -224,7 +224,7 @@ const NavLinks = ({
|
|||||||
{ route: Routes.VESTING, text: t('Vesting') },
|
{ route: Routes.VESTING, text: t('Vesting') },
|
||||||
{ route: Routes.STAKING, text: t('Staking') },
|
{ route: Routes.STAKING, text: t('Staking') },
|
||||||
{ route: Routes.REWARDS, text: t('Rewards') },
|
{ route: Routes.REWARDS, text: t('Rewards') },
|
||||||
{ route: Routes.WITHDRAW, text: t('Withdraw') },
|
{ route: Routes.WITHDRAWALS, text: t('Withdraw') },
|
||||||
{ route: Routes.GOVERNANCE, text: t('Governance') },
|
{ route: Routes.GOVERNANCE, text: t('Governance') },
|
||||||
];
|
];
|
||||||
const navClasses = classNames('flex', {
|
const navClasses = classNames('flex', {
|
||||||
|
@ -452,7 +452,7 @@
|
|||||||
"withdrawFormAmountLabel": "How much would you like to withdraw?",
|
"withdrawFormAmountLabel": "How much would you like to withdraw?",
|
||||||
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
|
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
|
||||||
"withdrawFormSubmitButtonPending": "Preparing",
|
"withdrawFormSubmitButtonPending": "Preparing",
|
||||||
"withdrawalsTitle": "Incomplete withdrawals",
|
"withdrawalsTitle": "Withdrawals",
|
||||||
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
|
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
|
||||||
"withdrawalsNone": "You don't have any pending withdrawals.",
|
"withdrawalsNone": "You don't have any pending withdrawals.",
|
||||||
"withdrawalsCompleteButton": "Finish withdrawal",
|
"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(
|
const LazyWithdrawals = React.lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
@ -201,11 +194,6 @@ const routerConfig = [
|
|||||||
name: 'Rewards',
|
name: 'Rewards',
|
||||||
component: LazyRewards,
|
component: LazyRewards,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: Routes.WITHDRAW,
|
|
||||||
name: 'Withdraw',
|
|
||||||
component: LazyWithdraw,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: Routes.WITHDRAWALS,
|
path: Routes.WITHDRAWALS,
|
||||||
name: 'Withdrawals',
|
name: 'Withdrawals',
|
||||||
|
@ -4,7 +4,6 @@ export default {
|
|||||||
CLAIM: '/claim',
|
CLAIM: '/claim',
|
||||||
STAKING: '/staking',
|
STAKING: '/staking',
|
||||||
REWARDS: '/rewards',
|
REWARDS: '/rewards',
|
||||||
WITHDRAW: '/withdraw',
|
|
||||||
WITHDRAWALS: '/withdrawals',
|
WITHDRAWALS: '/withdrawals',
|
||||||
GOVERNANCE: '/governance',
|
GOVERNANCE: '/governance',
|
||||||
VESTING: '/vesting',
|
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 { 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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
|
||||||
import { Heading } from '../../components/heading';
|
import { Heading } from '../../components/heading';
|
||||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { SplashLoader } from '../../components/splash-loader';
|
import { SplashLoader } from '../../components/splash-loader';
|
||||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||||
import { BigNumber } from '../../lib/bignumber';
|
import {
|
||||||
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
|
useWithdrawals,
|
||||||
import { addDecimal } from '../../lib/decimals';
|
WithdrawalDialogs,
|
||||||
import { truncateMiddle } from '../../lib/truncate-middle';
|
WithdrawalsTable,
|
||||||
import type { Withdrawals_party_withdrawals } from '@vegaprotocol/withdraws';
|
} from '@vegaprotocol/withdraws';
|
||||||
import { useCompleteWithdraw, useWithdrawals } from '@vegaprotocol/withdraws';
|
import { useState } from 'react';
|
||||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
const Withdrawals = () => {
|
const Withdrawals = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -32,19 +25,9 @@ const Withdrawals = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const WithdrawPendingContainer = () => {
|
const WithdrawPendingContainer = () => {
|
||||||
|
const [withdrawDialog, setWithdrawDialog] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { submit, Dialog } = useCompleteWithdraw();
|
const { withdrawals, loading, error } = useWithdrawals();
|
||||||
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]);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -55,7 +38,7 @@ const WithdrawPendingContainer = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || !data) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
<SplashLoader />
|
<SplashLoader />
|
||||||
@ -63,122 +46,23 @@ const WithdrawPendingContainer = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!withdrawals.length) {
|
|
||||||
return <p>{t('withdrawalsNone')}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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>{t('withdrawalsText')}</p>
|
||||||
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
|
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
|
||||||
<ul role="list">
|
<div className="w-full h-[500px]">
|
||||||
{withdrawals.map((w) => (
|
<WithdrawalsTable withdrawals={withdrawals} />
|
||||||
<li key={w.id} className="mb-10 last:mb-0">
|
</div>
|
||||||
<Withdrawal withdrawal={w} complete={submit} />
|
<WithdrawalDialogs
|
||||||
</li>
|
withdrawDialog={withdrawDialog}
|
||||||
))}
|
setWithdrawDialog={setWithdrawDialog}
|
||||||
</ul>
|
/>
|
||||||
<Dialog />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
export default Withdrawals;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { aliasQuery } from '@vegaprotocol/cypress';
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||||
import { generateNetworkParameters } from '../support/mocks/generate-network-parameters';
|
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';
|
import { connectVegaWallet } from '../support/vega-wallet';
|
||||||
|
|
||||||
describe('withdraw', () => {
|
describe('withdraw', () => {
|
||||||
@ -16,10 +17,13 @@ describe('withdraw', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.mockWeb3Provider();
|
cy.mockWeb3Provider();
|
||||||
cy.mockGQL((req) => {
|
cy.mockGQL((req) => {
|
||||||
aliasQuery(req, 'WithdrawPageQuery', generateWithdrawPageQuery());
|
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||||
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
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
|
// Withdraw page requires vega wallet connection
|
||||||
connectVegaWallet();
|
connectVegaWallet();
|
||||||
@ -27,15 +31,14 @@ describe('withdraw', () => {
|
|||||||
// It also requires connection Ethereum wallet
|
// It also requires connection Ethereum wallet
|
||||||
connectEthereumWallet();
|
connectEthereumWallet();
|
||||||
|
|
||||||
cy.wait('@WithdrawPageQuery');
|
cy.mockGQL((req) => {
|
||||||
cy.contains('Withdraw');
|
aliasQuery(req, 'WithdrawFormQuery', generateWithdrawFormQuery());
|
||||||
|
});
|
||||||
|
cy.getByTestId('withdraw-dialog-button').click();
|
||||||
|
cy.wait('@WithdrawFormQuery');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('form validation', () => {
|
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(submitWithdrawBtn).click();
|
||||||
|
|
||||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||||
@ -82,9 +85,8 @@ describe('withdraw', () => {
|
|||||||
cy.getByTestId(submitWithdrawBtn).click();
|
cy.getByTestId(submitWithdrawBtn).click();
|
||||||
cy.getByTestId('dialog-title').should(
|
cy.getByTestId('dialog-title').should(
|
||||||
'have.text',
|
'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
|
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 merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
export const generateWithdrawPageQuery = (
|
export const generateWithdrawFormQuery = (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
override?: PartialDeep<any>
|
override?: PartialDeep<any>
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,55 +1,74 @@
|
|||||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
|
import type { Withdrawals } from '@vegaprotocol/withdraws';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
export const generateWithdrawals = (
|
export const generateWithdrawals = (override?: PartialDeep<Withdrawals>) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const defaultResult: Withdrawals = {
|
||||||
override?: PartialDeep<any>
|
|
||||||
) => {
|
|
||||||
const defaultResult = {
|
|
||||||
party: {
|
party: {
|
||||||
id: 'party-0',
|
id: 'party-0',
|
||||||
withdrawals: [
|
withdrawalsConnection: {
|
||||||
{
|
__typename: 'WithdrawalsConnection',
|
||||||
id: 'withdrawal-0',
|
edges: [
|
||||||
status: WithdrawalStatus.STATUS_FINALIZED,
|
{
|
||||||
amount: '100',
|
__typename: 'WithdrawalEdge',
|
||||||
txHash: null,
|
node: {
|
||||||
createdTimestamp: new Date('2022-02-02').toISOString(),
|
id: 'withdrawal-0',
|
||||||
withdrawnTimestamp: new Date('2022-02-02').toISOString(),
|
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||||
details: {
|
amount: '100',
|
||||||
__typename: 'Erc20WithdrawalDetails',
|
txHash: null,
|
||||||
receiverAddress: '0x72c22822A19D20DE7e426fB84aa047399Ddd8853',
|
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',
|
__typename: 'WithdrawalEdge',
|
||||||
id: 'asset-0',
|
node: {
|
||||||
symbol: 'AST0',
|
id: 'withdrawal-1',
|
||||||
decimals: 5,
|
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',
|
__typename: 'Party',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=TESTNET
|
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_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
||||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# App configuration variables
|
# App configuration variables
|
||||||
NX_VEGA_ENV=TESTNET
|
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_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
|
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 { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const DepositsContainer = () => {
|
export const DepositsContainer = () => {
|
||||||
|
const { deposits, loading, error } = useDeposits();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[1fr_min-content] gap-4 h-full">
|
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||||
<div />
|
<header className="flex justify-between items-center p-4">
|
||||||
<div className="p-4">
|
<h4 className="text-lg text-black dark:text-white">{t('Deposits')}</h4>
|
||||||
<Link href="/portfolio/deposit" passHref={true}>
|
<Link href="/portfolio/deposit" passHref={true}>
|
||||||
<Button size="md" data-testid="deposit">
|
<Button>Deposit</Button>
|
||||||
{t('Deposit')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<AsyncRenderer
|
||||||
|
data={deposits}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
render={(data) => {
|
||||||
|
return <DepositsTable deposits={data} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Web3Container } from '@vegaprotocol/web3';
|
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||||
@ -67,14 +66,12 @@ const Portfolio = () => {
|
|||||||
</VegaWalletContainer>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="deposits" name={t('Deposits')}>
|
<Tab id="deposits" name={t('Deposits')}>
|
||||||
<DepositsContainer />
|
<VegaWalletContainer>
|
||||||
|
<DepositsContainer />
|
||||||
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="withdrawals" name={t('Withdrawals')}>
|
<Tab id="withdrawals" name={t('Withdrawals')}>
|
||||||
<Web3Container>
|
<WithdrawalsContainer />
|
||||||
<VegaWalletContainer>
|
|
||||||
<WithdrawalsContainer />
|
|
||||||
</VegaWalletContainer>
|
|
||||||
</Web3Container>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</PortfolioGridChild>
|
</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 { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useWithdrawals, WithdrawalsTable } from '@vegaprotocol/withdraws';
|
import {
|
||||||
import Link from 'next/link';
|
useWithdrawals,
|
||||||
|
WithdrawalDialogs,
|
||||||
|
WithdrawalsTable,
|
||||||
|
} from '@vegaprotocol/withdraws';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
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 = () => {
|
export const WithdrawalsContainer = () => {
|
||||||
const { data, loading, error } = useWithdrawals();
|
const { withdrawals, loading, error } = useWithdrawals();
|
||||||
|
const [withdrawDialog, setWithdrawDialog] = useState(false);
|
||||||
|
|
||||||
|
console.log('render');
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<Web3Container>
|
||||||
data={data}
|
<VegaWalletContainer>
|
||||||
loading={loading}
|
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||||
error={error}
|
<header className="flex justify-between items-center p-4">
|
||||||
render={(data) => {
|
<h4 className="text-lg text-black dark:text-white">
|
||||||
const withdrawals = orderBy(
|
{t('Withdrawals')}
|
||||||
data.party?.withdrawals || [],
|
</h4>
|
||||||
(w) => new Date(w.createdTimestamp).getTime(),
|
<Button
|
||||||
'desc'
|
onClick={() => setWithdrawDialog(true)}
|
||||||
);
|
data-testid="withdraw-dialog-button"
|
||||||
return (
|
>
|
||||||
<div className="grid grid-cols-[1fr_min-content] gap-4 h-full">
|
{t('Withdraw')}
|
||||||
<WithdrawalsTable withdrawals={withdrawals} />
|
</Button>
|
||||||
<div className="p-4">
|
</header>
|
||||||
<Link href="/portfolio/withdraw" passHref={true}>
|
<div>
|
||||||
<Button size="md" data-testid="start-withdrawal">
|
<AsyncRenderer
|
||||||
{t('Withdraw')}
|
data={withdrawals}
|
||||||
</Button>
|
loading={loading}
|
||||||
</Link>
|
error={error}
|
||||||
</div>
|
render={(data) => {
|
||||||
|
return <WithdrawalsTable withdrawals={data} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</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,
|
market,
|
||||||
children,
|
children,
|
||||||
}: DealTicketManagerProps) => {
|
}: DealTicketManagerProps) => {
|
||||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||||
useOrderSubmit();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -33,13 +32,13 @@ export const DealTicketManager = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TransactionDialog
|
<Dialog
|
||||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||||
>
|
>
|
||||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||||
</TransactionDialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export * from './lib/deposit-manager';
|
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(
|
expect(screen.getByTestId('dialog-title')).toHaveTextContent(
|
||||||
'Proposal rejected'
|
'Proposal submitted'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
getProposalDialogTitle,
|
getProposalDialogTitle,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export interface FormFields {
|
export interface FormFields {
|
||||||
proposalData: string;
|
proposalData: string;
|
||||||
@ -24,7 +23,7 @@ export const ProposalForm = () => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting, errors },
|
formState: { isSubmitting, errors },
|
||||||
} = useForm<FormFields>();
|
} = useForm<FormFields>();
|
||||||
const { finalizedProposal, submit, TransactionDialog } = useProposalSubmit();
|
const { finalizedProposal, submit, Dialog } = useProposalSubmit();
|
||||||
|
|
||||||
const hasError = Boolean(errors.proposalData?.message);
|
const hasError = Boolean(errors.proposalData?.message);
|
||||||
|
|
||||||
@ -63,31 +62,23 @@ export const ProposalForm = () => {
|
|||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<span className="my-20">
|
<Button
|
||||||
<Button
|
variant="primary"
|
||||||
variant="primary"
|
type="submit"
|
||||||
type="submit"
|
data-testid="proposal-submit"
|
||||||
data-testid="proposal-submit"
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
>
|
||||||
>
|
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
||||||
{isSubmitting ? t('Submitting') : t('Submit')} {t('Proposal')}
|
</Button>
|
||||||
</Button>
|
<Dialog
|
||||||
</span>
|
title={getProposalDialogTitle(finalizedProposal?.state)}
|
||||||
{finalizedProposal?.rejectionReason ? (
|
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
||||||
<TransactionDialog
|
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
||||||
title={t('Proposal rejected')}
|
>
|
||||||
intent={getProposalDialogIntent(ProposalState.STATE_REJECTED)}
|
{finalizedProposal?.rejectionReason ? (
|
||||||
icon={getProposalDialogIcon(ProposalState.STATE_REJECTED)}
|
|
||||||
>
|
|
||||||
<p>{finalizedProposal.rejectionReason}</p>
|
<p>{finalizedProposal.rejectionReason}</p>
|
||||||
</TransactionDialog>
|
) : undefined}
|
||||||
) : (
|
</Dialog>
|
||||||
<TransactionDialog
|
|
||||||
title={getProposalDialogTitle(finalizedProposal?.state)}
|
|
||||||
intent={getProposalDialogIntent(finalizedProposal?.state)}
|
|
||||||
icon={getProposalDialogIcon(finalizedProposal?.state)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,8 +10,7 @@ export const useProposalSubmit = () => {
|
|||||||
const { keypair } = useVegaWallet();
|
const { keypair } = useVegaWallet();
|
||||||
const waitForProposalEvent = useProposalEvent();
|
const waitForProposalEvent = useProposalEvent();
|
||||||
|
|
||||||
const { send, transaction, setComplete, TransactionDialog } =
|
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
|
||||||
useVegaTransaction();
|
|
||||||
|
|
||||||
const [finalizedProposal, setFinalizedProposal] =
|
const [finalizedProposal, setFinalizedProposal] =
|
||||||
useState<ProposalEvent_busEvents_event_Proposal | null>(null);
|
useState<ProposalEvent_busEvents_event_Proposal | null>(null);
|
||||||
@ -52,7 +51,7 @@ export const useProposalSubmit = () => {
|
|||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
finalizedProposal,
|
finalizedProposal,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
submit,
|
submit,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -62,7 +62,7 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
setEditOrder={setEditOrder}
|
setEditOrder={setEditOrder}
|
||||||
/>
|
/>
|
||||||
<orderCancel.TransactionDialog
|
<orderCancel.Dialog
|
||||||
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||||
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||||
>
|
>
|
||||||
@ -70,15 +70,15 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
|||||||
transaction={orderCancel.transaction}
|
transaction={orderCancel.transaction}
|
||||||
order={orderCancel.cancelledOrder}
|
order={orderCancel.cancelledOrder}
|
||||||
/>
|
/>
|
||||||
</orderCancel.TransactionDialog>
|
</orderCancel.Dialog>
|
||||||
<orderEdit.TransactionDialog
|
<orderEdit.Dialog
|
||||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||||
>
|
>
|
||||||
<OrderFeedback
|
<OrderFeedback
|
||||||
transaction={orderEdit.transaction}
|
transaction={orderEdit.transaction}
|
||||||
order={orderEdit.updatedOrder}
|
order={orderEdit.updatedOrder}
|
||||||
/>
|
/>
|
||||||
</orderEdit.TransactionDialog>
|
</orderEdit.Dialog>
|
||||||
{editOrder && (
|
{editOrder && (
|
||||||
<OrderEditDialog
|
<OrderEditDialog
|
||||||
isOpen={Boolean(editOrder)}
|
isOpen={Boolean(editOrder)}
|
||||||
|
@ -21,7 +21,7 @@ export const useOrderCancel = () => {
|
|||||||
transaction,
|
transaction,
|
||||||
reset: resetTransaction,
|
reset: resetTransaction,
|
||||||
setComplete,
|
setComplete,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
} = useVegaTransaction();
|
} = useVegaTransaction();
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
@ -62,7 +62,7 @@ export const useOrderCancel = () => {
|
|||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
cancelledOrder,
|
cancelledOrder,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
cancel,
|
cancel,
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@ export const useOrderEdit = (order: OrderFields | null) => {
|
|||||||
transaction,
|
transaction,
|
||||||
reset: resetTransaction,
|
reset: resetTransaction,
|
||||||
setComplete,
|
setComplete,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
} = useVegaTransaction();
|
} = useVegaTransaction();
|
||||||
|
|
||||||
const waitForOrderEvent = useOrderEvent();
|
const waitForOrderEvent = useOrderEvent();
|
||||||
@ -71,7 +71,7 @@ export const useOrderEdit = (order: OrderFields | null) => {
|
|||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
updatedOrder,
|
updatedOrder,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
edit,
|
edit,
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
|
@ -102,7 +102,7 @@ export const useOrderSubmit = () => {
|
|||||||
transaction,
|
transaction,
|
||||||
reset: resetTransaction,
|
reset: resetTransaction,
|
||||||
setComplete,
|
setComplete,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
} = useVegaTransaction();
|
} = useVegaTransaction();
|
||||||
|
|
||||||
const [finalizedOrder, setFinalizedOrder] =
|
const [finalizedOrder, setFinalizedOrder] =
|
||||||
@ -158,7 +158,7 @@ export const useOrderSubmit = () => {
|
|||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
finalizedOrder,
|
finalizedOrder,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
submit,
|
submit,
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@ const getSymbols = (positions: Position[]) =>
|
|||||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
const assetSymbols = useRef<string[] | undefined>();
|
const assetSymbols = useRef<string[] | undefined>();
|
||||||
const { submit, TransactionDialog } = useClosePosition();
|
const { submit, Dialog } = useClosePosition();
|
||||||
const onClose = useCallback(
|
const onClose = useCallback(
|
||||||
(position: Position) => {
|
(position: Position) => {
|
||||||
submit(position);
|
submit(position);
|
||||||
@ -55,9 +55,9 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
<TransactionDialog>
|
<Dialog>
|
||||||
<p>Your position was not closed! This is still not implemented. </p>
|
<p>Your position was not closed! This is still not implemented. </p>
|
||||||
</TransactionDialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ export const useClosePosition = () => {
|
|||||||
transaction,
|
transaction,
|
||||||
reset: resetTransaction,
|
reset: resetTransaction,
|
||||||
setComplete,
|
setComplete,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
} = useVegaTransaction();
|
} = useVegaTransaction();
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
@ -57,7 +57,7 @@ export const useClosePosition = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
transaction,
|
transaction,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
submit,
|
submit,
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,9 @@ export class CollateralBridge {
|
|||||||
get_withdraw_threshold(assetSource: string) {
|
get_withdraw_threshold(assetSource: string) {
|
||||||
return this.contract.get_withdraw_threshold(assetSource);
|
return this.contract.get_withdraw_threshold(assetSource);
|
||||||
}
|
}
|
||||||
|
default_withdraw_delay() {
|
||||||
|
return this.contract.default_withdraw_delay();
|
||||||
|
}
|
||||||
withdraw_asset(
|
withdraw_asset(
|
||||||
assetSource: string,
|
assetSource: string,
|
||||||
amount: 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-lazy';
|
||||||
export * from './ag-grid-dynamic';
|
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]
|
[sendTx, setTransaction, reset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const TransactionDialog = useMemo(() => {
|
const Dialog = useMemo(() => {
|
||||||
return (props: DialogProps) => (
|
return (props: DialogProps) => (
|
||||||
<VegaTransactionDialog
|
<VegaTransactionDialog
|
||||||
{...props}
|
{...props}
|
||||||
@ -122,7 +122,7 @@ export const useVegaTransaction = () => {
|
|||||||
reset,
|
reset,
|
||||||
setComplete,
|
setComplete,
|
||||||
setTransaction,
|
setTransaction,
|
||||||
TransactionDialog,
|
Dialog,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ export const VegaTransactionDialog = ({
|
|||||||
intent={computedIntent}
|
intent={computedIntent}
|
||||||
title={computedTitle}
|
title={computedTitle}
|
||||||
icon={computedIcon}
|
icon={computedIcon}
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@ -59,8 +60,9 @@ interface VegaDialogProps {
|
|||||||
export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
||||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||||
|
|
||||||
|
let content = null;
|
||||||
if (transaction.status === VegaTxStatus.Requested) {
|
if (transaction.status === VegaTxStatus.Requested) {
|
||||||
return (
|
content = (
|
||||||
<p data-testid={transaction.status}>
|
<p data-testid={transaction.status}>
|
||||||
{t(
|
{t(
|
||||||
'Please open your wallet application and confirm or reject the transaction'
|
'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) {
|
if (transaction.status === VegaTxStatus.Error) {
|
||||||
return (
|
content = (
|
||||||
<div data-testid={transaction.status}>
|
<div data-testid={transaction.status}>
|
||||||
<p>{transaction.error && formatLabel(transaction.error)}</p>
|
<p>{transaction.error && formatLabel(transaction.error)}</p>
|
||||||
{transaction.details && (
|
{transaction.details && (
|
||||||
@ -81,7 +83,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (transaction.status === VegaTxStatus.Pending) {
|
if (transaction.status === VegaTxStatus.Pending) {
|
||||||
return (
|
content = (
|
||||||
<div data-testid={transaction.status}>
|
<div data-testid={transaction.status}>
|
||||||
<p className="break-all">
|
<p className="break-all">
|
||||||
{t('Please wait for your transaction to be confirmed')} -
|
{t('Please wait for your transaction to be confirmed')} -
|
||||||
@ -102,7 +104,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (transaction.status === VegaTxStatus.Complete) {
|
if (transaction.status === VegaTxStatus.Complete) {
|
||||||
return (
|
content = (
|
||||||
<div data-testid={transaction.status}>
|
<div data-testid={transaction.status}>
|
||||||
<p className="break-all">
|
<p className="break-all">
|
||||||
{t('Your transaction has been confirmed')} -
|
{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) => {
|
const getIntent = (transaction: VegaTxState) => {
|
||||||
@ -160,7 +162,11 @@ const getIcon = (transaction: VegaTxState) => {
|
|||||||
case VegaTxStatus.Requested:
|
case VegaTxStatus.Requested:
|
||||||
return <Icon name="hand-up" />;
|
return <Icon name="hand-up" />;
|
||||||
case VegaTxStatus.Pending:
|
case VegaTxStatus.Pending:
|
||||||
return <Loader size="small" />;
|
return (
|
||||||
|
<span className="mt-1">
|
||||||
|
<Loader size="small" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
case VegaTxStatus.Error:
|
case VegaTxStatus.Error:
|
||||||
return <Icon name="warning-sign" />;
|
return <Icon name="warning-sign" />;
|
||||||
case VegaTxStatus.Complete:
|
case VegaTxStatus.Complete:
|
||||||
|
@ -6,7 +6,7 @@ export * from './lib/use-token-decimals';
|
|||||||
export * from './lib/use-ethereum-config';
|
export * from './lib/use-ethereum-config';
|
||||||
export * from './lib/use-ethereum-read-contract';
|
export * from './lib/use-ethereum-read-contract';
|
||||||
export * from './lib/use-ethereum-transaction';
|
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-provider';
|
||||||
export * from './lib/web3-connect-dialog';
|
export * from './lib/web3-connect-dialog';
|
||||||
export * from './lib/web3-wallet-input';
|
export * from './lib/web3-wallet-input';
|
||||||
|
@ -3,8 +3,8 @@ import merge from 'lodash/merge';
|
|||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import { EthereumError } from '../ethereum-error';
|
import { EthereumError } from '../ethereum-error';
|
||||||
import { EthTxStatus } from '../use-ethereum-transaction';
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
import type { TransactionDialogProps } from './transaction-dialog';
|
import type { EthereumTransactionDialogProps } from './ethereum-transaction-dialog';
|
||||||
import { TransactionDialog } from './transaction-dialog';
|
import { EthereumTransactionDialog } from './ethereum-transaction-dialog';
|
||||||
|
|
||||||
jest.mock('@web3-react/core', () => ({
|
jest.mock('@web3-react/core', () => ({
|
||||||
useWeb3React: () => ({
|
useWeb3React: () => ({
|
||||||
@ -12,11 +12,11 @@ jest.mock('@web3-react/core', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let props: TransactionDialogProps;
|
let props: EthereumTransactionDialogProps;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
name: 'test',
|
title: 'test',
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
transaction: {
|
transaction: {
|
||||||
status: EthTxStatus.Default,
|
status: EthTxStatus.Default,
|
||||||
@ -29,9 +29,11 @@ beforeEach(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateJsx = (moreProps?: PartialDeep<TransactionDialogProps>) => {
|
const generateJsx = (
|
||||||
|
moreProps?: PartialDeep<EthereumTransactionDialogProps>
|
||||||
|
) => {
|
||||||
const mergedProps = merge(props, moreProps);
|
const mergedProps = merge(props, moreProps);
|
||||||
return <TransactionDialog {...mergedProps} />;
|
return <EthereumTransactionDialog {...mergedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
it('Opens when tx starts and closes if the user rejects the tx', () => {
|
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();
|
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(
|
const { container, rerender } = render(
|
||||||
generateJsx({ transaction: { status: EthTxStatus.Pending } })
|
generateJsx({ transaction: { status: EthTxStatus.Pending } })
|
||||||
);
|
);
|
||||||
@ -87,7 +89,7 @@ it('Dialog states', () => {
|
|||||||
transaction: { status: EthTxStatus.Pending, confirmations: 0 },
|
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('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('Awaiting Ethereum transaction 0/1 confirmations...')
|
screen.getByText('Awaiting Ethereum transaction 0/1 confirmations...')
|
||||||
@ -100,7 +102,7 @@ it('Dialog states', () => {
|
|||||||
transaction: { status: EthTxStatus.Complete, confirmations: 1 },
|
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('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ethereum transaction complete')).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('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Transaction confirmed')).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();
|
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 type { EthereumError } from './ethereum-error';
|
||||||
import { isExpectedEthereumError } from './ethereum-error';
|
import { isExpectedEthereumError } from './ethereum-error';
|
||||||
import { isEthereumError } from './ethereum-error';
|
import { isEthereumError } from './ethereum-error';
|
||||||
import { TransactionDialog } from './transaction-dialog';
|
import { EthereumTransactionDialog } from './ethereum-transaction-dialog';
|
||||||
|
|
||||||
export enum EthTxStatus {
|
export enum EthTxStatus {
|
||||||
Default = 'Default',
|
Default = 'Default',
|
||||||
@ -152,8 +152,8 @@ export const useEthereumTransaction = <
|
|||||||
|
|
||||||
const Dialog = useMemo(() => {
|
const Dialog = useMemo(() => {
|
||||||
return () => (
|
return () => (
|
||||||
<TransactionDialog
|
<EthereumTransactionDialog
|
||||||
name={formatLabel(methodName as string)}
|
title={formatLabel(methodName as string)}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
export * from './lib/withdrawal-dialogs';
|
||||||
export * from './lib/withdraw-form';
|
export * from './lib/withdraw-form';
|
||||||
|
export * from './lib/withdraw-form-container';
|
||||||
export * from './lib/withdraw-manager';
|
export * from './lib/withdraw-manager';
|
||||||
export * from './lib/withdrawals-table';
|
export * from './lib/withdrawals-table';
|
||||||
|
export * from './lib/withdrawal-feedback';
|
||||||
export * from './lib/use-complete-withdraw';
|
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/use-withdrawals';
|
||||||
export * from './lib/__generated__/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";
|
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";
|
__typename: "Withdrawal";
|
||||||
/**
|
/**
|
||||||
* The Vega internal ID of the withdrawal
|
* The Vega internal ID of the withdrawal
|
||||||
@ -21,7 +21,7 @@ export interface WithdrawPageQuery_party_withdrawals {
|
|||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawPageQuery_party_accounts_asset {
|
export interface WithdrawFormQuery_party_accounts_asset {
|
||||||
__typename: "Asset";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The ID of the asset
|
* The ID of the asset
|
||||||
@ -33,7 +33,7 @@ export interface WithdrawPageQuery_party_accounts_asset {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawPageQuery_party_accounts {
|
export interface WithdrawFormQuery_party_accounts {
|
||||||
__typename: "Account";
|
__typename: "Account";
|
||||||
/**
|
/**
|
||||||
* Account type (General, Margin, etc)
|
* Account type (General, Margin, etc)
|
||||||
@ -46,10 +46,10 @@ export interface WithdrawPageQuery_party_accounts {
|
|||||||
/**
|
/**
|
||||||
* Asset, the 'currency'
|
* Asset, the 'currency'
|
||||||
*/
|
*/
|
||||||
asset: WithdrawPageQuery_party_accounts_asset;
|
asset: WithdrawFormQuery_party_accounts_asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawPageQuery_party {
|
export interface WithdrawFormQuery_party {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
* Party identifier
|
* Party identifier
|
||||||
@ -58,18 +58,18 @@ export interface WithdrawPageQuery_party {
|
|||||||
/**
|
/**
|
||||||
* The list of all withdrawals initiated by the 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
|
* 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";
|
__typename: "BuiltinAsset";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawPageQuery_assetsConnection_edges_node_source_ERC20 {
|
export interface WithdrawFormQuery_assetsConnection_edges_node_source_ERC20 {
|
||||||
__typename: "ERC20";
|
__typename: "ERC20";
|
||||||
/**
|
/**
|
||||||
* The address of the ERC20 contract
|
* The address of the ERC20 contract
|
||||||
@ -77,9 +77,9 @@ export interface WithdrawPageQuery_assetsConnection_edges_node_source_ERC20 {
|
|||||||
contractAddress: string;
|
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";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The ID of the 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)
|
* 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";
|
__typename: "AssetEdge";
|
||||||
node: WithdrawPageQuery_assetsConnection_edges_node;
|
node: WithdrawFormQuery_assetsConnection_edges_node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawPageQuery_assetsConnection {
|
export interface WithdrawFormQuery_assetsConnection {
|
||||||
__typename: "AssetsConnection";
|
__typename: "AssetsConnection";
|
||||||
/**
|
/**
|
||||||
* The assets
|
* 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
|
* 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
|
* 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;
|
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";
|
__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 {
|
export interface WithdrawalEvent_busEvents_event_Withdrawal_asset {
|
||||||
__typename: "Asset";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The ID of the asset
|
* The ID of the asset
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
|
/**
|
||||||
|
* The full name of the asset (e.g: Great British Pound)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The symbol of the asset (e.g: GBP)
|
* 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
|
* 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;
|
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 {
|
export interface WithdrawalEvent_busEvents_event_Withdrawal_details {
|
||||||
|
@ -9,12 +9,30 @@ import { WithdrawalStatus } from "@vegaprotocol/types";
|
|||||||
// GraphQL fragment: WithdrawalFields
|
// 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 {
|
export interface WithdrawalFields_asset {
|
||||||
__typename: "Asset";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The ID of the asset
|
* The ID of the asset
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
|
/**
|
||||||
|
* The full name of the asset (e.g: Great British Pound)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The symbol of the asset (e.g: GBP)
|
* 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
|
* 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;
|
decimals: number;
|
||||||
|
/**
|
||||||
|
* The origin source of the asset (e.g: an ERC20 asset)
|
||||||
|
*/
|
||||||
|
source: WithdrawalFields_asset_source;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithdrawalFields_details {
|
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
|
// 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";
|
__typename: "Asset";
|
||||||
/**
|
/**
|
||||||
* The ID of the asset
|
* The ID of the asset
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
|
/**
|
||||||
|
* The full name of the asset (e.g: Great British Pound)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The symbol of the asset (e.g: GBP)
|
* 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
|
* 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;
|
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";
|
__typename: "Erc20WithdrawalDetails";
|
||||||
/**
|
/**
|
||||||
* The ethereum address of the receiver of the asset funds
|
* The ethereum address of the receiver of the asset funds
|
||||||
@ -33,7 +55,7 @@ export interface Withdrawals_party_withdrawals_details {
|
|||||||
receiverAddress: string;
|
receiverAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Withdrawals_party_withdrawals {
|
export interface Withdrawals_party_withdrawalsConnection_edges_node {
|
||||||
__typename: "Withdrawal";
|
__typename: "Withdrawal";
|
||||||
/**
|
/**
|
||||||
* The Vega internal ID of the withdrawal
|
* The Vega internal ID of the withdrawal
|
||||||
@ -50,7 +72,7 @@ export interface Withdrawals_party_withdrawals {
|
|||||||
/**
|
/**
|
||||||
* The asset to be withdrawn
|
* 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
|
* RFC3339Nano time at which the withdrawal was created
|
||||||
*/
|
*/
|
||||||
@ -66,13 +88,26 @@ export interface Withdrawals_party_withdrawals {
|
|||||||
/**
|
/**
|
||||||
* Foreign chain specific details about the withdrawal
|
* 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
|
* Whether or the not the withdrawal is being processed on Ethereum
|
||||||
*/
|
*/
|
||||||
pendingOnForeignChain: boolean;
|
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 {
|
export interface Withdrawals_party {
|
||||||
__typename: "Party";
|
__typename: "Party";
|
||||||
/**
|
/**
|
||||||
@ -82,7 +117,7 @@ export interface Withdrawals_party {
|
|||||||
/**
|
/**
|
||||||
* The list of all withdrawals initiated by the party
|
* The list of all withdrawals initiated by the party
|
||||||
*/
|
*/
|
||||||
withdrawals: Withdrawals_party_withdrawals[] | null;
|
withdrawalsConnection: Withdrawals_party_withdrawalsConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Withdrawals {
|
export interface Withdrawals {
|
||||||
|
@ -3,7 +3,7 @@ import { AccountType, WithdrawalStatus } from '@vegaprotocol/types';
|
|||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import type { Account } from './types';
|
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>) => {
|
export const generateAsset = (override?: PartialDeep<Asset>) => {
|
||||||
const defaultAsset: Asset = {
|
const defaultAsset: Asset = {
|
||||||
@ -33,8 +33,8 @@ export const generateAccount = (override?: PartialDeep<Account>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateWithdrawal = (
|
export const generateWithdrawal = (
|
||||||
override?: PartialDeep<Withdrawals_party_withdrawals>
|
override?: PartialDeep<Withdrawals_party_withdrawalsConnection_edges_node>
|
||||||
): Withdrawals_party_withdrawals => {
|
): Withdrawals_party_withdrawalsConnection_edges_node => {
|
||||||
return merge(
|
return merge(
|
||||||
{
|
{
|
||||||
__typename: 'Withdrawal',
|
__typename: 'Withdrawal',
|
||||||
@ -43,9 +43,14 @@ export const generateWithdrawal = (
|
|||||||
amount: '100',
|
amount: '100',
|
||||||
asset: {
|
asset: {
|
||||||
__typename: 'Asset',
|
__typename: 'Asset',
|
||||||
|
name: 'asset-name',
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
symbol: 'asset-symbol',
|
symbol: 'asset-symbol',
|
||||||
decimals: 2,
|
decimals: 2,
|
||||||
|
source: {
|
||||||
|
__typename: 'ERC20',
|
||||||
|
contractAddress: '0x123',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createdTimestamp: '2022-04-20T00:00:00',
|
createdTimestamp: '2022-04-20T00:00:00',
|
||||||
withdrawnTimestamp: null,
|
withdrawnTimestamp: null,
|
||||||
|
@ -10,13 +10,17 @@ import * as web3 from '@vegaprotocol/web3';
|
|||||||
import * as sentry from '@sentry/react';
|
import * as sentry from '@sentry/react';
|
||||||
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
import type { Erc20Approval_erc20WithdrawalApproval } from './__generated__/Erc20Approval';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/web3', () => ({
|
jest.mock('@vegaprotocol/web3', () => {
|
||||||
useBridgeContract: jest.fn().mockReturnValue({
|
const orig = jest.requireActual('@vegaprotocol/web3');
|
||||||
withdraw_asset: jest.fn(),
|
return {
|
||||||
isNewContract: true,
|
...orig,
|
||||||
}),
|
useBridgeContract: jest.fn().mockReturnValue({
|
||||||
useEthereumTransaction: jest.fn(),
|
withdraw_asset: jest.fn(),
|
||||||
}));
|
isNewContract: true,
|
||||||
|
}),
|
||||||
|
useEthereumTransaction: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function setup(mocks?: MockedResponse[]) {
|
function setup(mocks?: MockedResponse[]) {
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { gql, useApolloClient } from '@apollo/client';
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
import { captureException } from '@sentry/react';
|
import { captureException } from '@sentry/react';
|
||||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
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 { useCallback, useEffect, useState } from 'react';
|
||||||
import { ERC20_APPROVAL_QUERY } from './queries';
|
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||||
import type {
|
import type {
|
||||||
@ -21,7 +25,7 @@ export const useCompleteWithdraw = () => {
|
|||||||
const { query, cache } = useApolloClient();
|
const { query, cache } = useApolloClient();
|
||||||
const contract = useBridgeContract();
|
const contract = useBridgeContract();
|
||||||
const [id, setId] = useState('');
|
const [id, setId] = useState('');
|
||||||
const { transaction, perform, Dialog } = useEthereumTransaction<
|
const { transaction, perform, reset, Dialog } = useEthereumTransaction<
|
||||||
CollateralBridge,
|
CollateralBridge,
|
||||||
'withdraw_asset'
|
'withdraw_asset'
|
||||||
>(contract, 'withdraw_asset');
|
>(contract, 'withdraw_asset');
|
||||||
@ -29,6 +33,7 @@ export const useCompleteWithdraw = () => {
|
|||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
async (withdrawalId: string) => {
|
async (withdrawalId: string) => {
|
||||||
setId(withdrawalId);
|
setId(withdrawalId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return;
|
return;
|
||||||
@ -39,6 +44,7 @@ export const useCompleteWithdraw = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const approval = res.data.erc20WithdrawalApproval;
|
const approval = res.data.erc20WithdrawalApproval;
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
throw new Error('Could not retrieve withdrawal approval');
|
throw new Error('Could not retrieve withdrawal approval');
|
||||||
}
|
}
|
||||||
@ -65,12 +71,13 @@ export const useCompleteWithdraw = () => {
|
|||||||
fragment: PENDING_WITHDRAWAL_FRAGMMENT,
|
fragment: PENDING_WITHDRAWAL_FRAGMMENT,
|
||||||
data: {
|
data: {
|
||||||
__typename: 'Withdrawal',
|
__typename: 'Withdrawal',
|
||||||
pendingOnForeignChain: true,
|
pendingOnForeignChain:
|
||||||
|
transaction.status === EthTxStatus.Pending ? true : false,
|
||||||
txHash: transaction.txHash,
|
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';
|
} from './__generated__/WithdrawalEvent';
|
||||||
import type {
|
import type {
|
||||||
Withdrawals,
|
Withdrawals,
|
||||||
Withdrawals_party_withdrawals,
|
Withdrawals_party_withdrawalsConnection_edges_node,
|
||||||
} from './__generated__/Withdrawals';
|
} from './__generated__/Withdrawals';
|
||||||
|
|
||||||
describe('updateQuery', () => {
|
describe('updateQuery', () => {
|
||||||
it('Updates existing withdrawals', () => {
|
it('updates existing withdrawals', () => {
|
||||||
const withdrawal = generateWithdrawal({
|
const withdrawal = generateWithdrawal({
|
||||||
id: '1',
|
id: '1',
|
||||||
status: WithdrawalStatus.STATUS_OPEN,
|
status: WithdrawalStatus.STATUS_OPEN,
|
||||||
@ -27,7 +27,15 @@ describe('updateQuery', () => {
|
|||||||
party: {
|
party: {
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
id: 'party-id',
|
id: 'party-id',
|
||||||
withdrawals: [withdrawalUpdate],
|
withdrawalsConnection: {
|
||||||
|
__typename: 'WithdrawalsConnection',
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
__typename: 'WithdrawalEdge',
|
||||||
|
node: withdrawalUpdate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -48,21 +56,45 @@ describe('updateQuery', () => {
|
|||||||
party: {
|
party: {
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
id: 'party-id',
|
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({
|
const withdrawalUpdate = generateWithdrawal({
|
||||||
id: '2',
|
id: '2',
|
||||||
});
|
});
|
||||||
const incoming = mockSub([withdrawalUpdate]);
|
const incoming = mockSub([withdrawalUpdate]);
|
||||||
|
|
||||||
expect(updateQuery({ party: null }, incoming)).toEqual({
|
expect(
|
||||||
|
updateQuery({ party: null }, { ...incoming, variables: { partyId } })
|
||||||
|
).toEqual({
|
||||||
party: {
|
party: {
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
withdrawals: [withdrawalUpdate],
|
id: partyId,
|
||||||
|
withdrawalsConnection: {
|
||||||
|
__typename: 'WithdrawalsConnection',
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
__typename: 'WithdrawalEdge',
|
||||||
|
node: withdrawalUpdate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -89,20 +121,42 @@ describe('updateQuery', () => {
|
|||||||
party: {
|
party: {
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
id: 'party-id',
|
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 = (
|
const mockQuery = (
|
||||||
withdrawals: Withdrawals_party_withdrawals[]
|
withdrawals: Withdrawals_party_withdrawalsConnection_edges_node[]
|
||||||
): Withdrawals => {
|
): Withdrawals => {
|
||||||
return {
|
return {
|
||||||
party: {
|
party: {
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
id: 'party-id',
|
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 { gql, useQuery } from '@apollo/client';
|
||||||
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import type {
|
import type {
|
||||||
WithdrawalEvent,
|
WithdrawalEvent,
|
||||||
WithdrawalEventVariables,
|
WithdrawalEventVariables,
|
||||||
@ -12,6 +14,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
Withdrawals,
|
Withdrawals,
|
||||||
WithdrawalsVariables,
|
WithdrawalsVariables,
|
||||||
|
Withdrawals_party_withdrawalsConnection_edges,
|
||||||
} from './__generated__/Withdrawals';
|
} from './__generated__/Withdrawals';
|
||||||
|
|
||||||
const WITHDRAWAL_FRAGMENT = gql`
|
const WITHDRAWAL_FRAGMENT = gql`
|
||||||
@ -21,8 +24,14 @@ const WITHDRAWAL_FRAGMENT = gql`
|
|||||||
amount
|
amount
|
||||||
asset {
|
asset {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
symbol
|
symbol
|
||||||
decimals
|
decimals
|
||||||
|
source {
|
||||||
|
... on ERC20 {
|
||||||
|
contractAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
createdTimestamp
|
createdTimestamp
|
||||||
withdrawnTimestamp
|
withdrawnTimestamp
|
||||||
@ -41,8 +50,12 @@ export const WITHDRAWALS_QUERY = gql`
|
|||||||
query Withdrawals($partyId: ID!) {
|
query Withdrawals($partyId: ID!) {
|
||||||
party(id: $partyId) {
|
party(id: $partyId) {
|
||||||
id
|
id
|
||||||
withdrawals {
|
withdrawalsConnection {
|
||||||
...WithdrawalFields
|
edges {
|
||||||
|
node {
|
||||||
|
...WithdrawalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +76,7 @@ export const WITHDRAWAL_BUS_EVENT_SUB = gql`
|
|||||||
|
|
||||||
export const useWithdrawals = () => {
|
export const useWithdrawals = () => {
|
||||||
const { keypair } = useVegaWallet();
|
const { keypair } = useVegaWallet();
|
||||||
const { subscribeToMore, ...queryResult } = useQuery<
|
const { data, loading, error, subscribeToMore } = useQuery<
|
||||||
Withdrawals,
|
Withdrawals,
|
||||||
WithdrawalsVariables
|
WithdrawalsVariables
|
||||||
>(WITHDRAWALS_QUERY, {
|
>(WITHDRAWALS_QUERY, {
|
||||||
@ -85,19 +98,36 @@ export const useWithdrawals = () => {
|
|||||||
};
|
};
|
||||||
}, [keypair?.pub, subscribeToMore]);
|
}, [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<
|
export const updateQuery: UpdateQueryFn<
|
||||||
Withdrawals,
|
Withdrawals,
|
||||||
WithdrawalEventVariables,
|
WithdrawalEventVariables,
|
||||||
WithdrawalEvent
|
WithdrawalEvent
|
||||||
> = (prev, { subscriptionData }) => {
|
> = (prev, { subscriptionData, variables }) => {
|
||||||
if (!subscriptionData.data.busEvents?.length) {
|
if (!subscriptionData.data.busEvents?.length) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
const curr = prev.party?.withdrawals || [];
|
const curr = prev.party?.withdrawalsConnection.edges || [];
|
||||||
const incoming = subscriptionData.data.busEvents
|
const incoming = subscriptionData.data.busEvents
|
||||||
.map((e) => {
|
.map((e) => {
|
||||||
return {
|
return {
|
||||||
@ -105,21 +135,28 @@ export const updateQuery: UpdateQueryFn<
|
|||||||
pendingOnForeignChain: false,
|
pendingOnForeignChain: false,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter(isWithdrawalEvent)
|
||||||
isWithdrawalEvent
|
.map(
|
||||||
// Need this type cast here, TS can't infer that we've filtered any event types
|
(w) =>
|
||||||
// that arent Withdrawals
|
({
|
||||||
) as WithdrawalEvent_busEvents_event_Withdrawal[];
|
__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
|
// Write new party to cache if not present
|
||||||
if (!prev.party) {
|
if (!prev.party) {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
party: {
|
party: {
|
||||||
|
id: variables?.partyId,
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
withdrawals,
|
withdrawalsConnection: {
|
||||||
|
__typename: 'WithdrawalsConnection',
|
||||||
|
edges,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as Withdrawals;
|
} as Withdrawals;
|
||||||
}
|
}
|
||||||
@ -128,7 +165,10 @@ export const updateQuery: UpdateQueryFn<
|
|||||||
...prev,
|
...prev,
|
||||||
party: {
|
party: {
|
||||||
...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 = {
|
props = {
|
||||||
assets,
|
assets,
|
||||||
min: new BigNumber(0.00001),
|
min: new BigNumber(0.00001),
|
||||||
max: {
|
balance: new BigNumber(100),
|
||||||
balance: new BigNumber(100),
|
threshold: new BigNumber(200),
|
||||||
threshold: new BigNumber(200),
|
delay: 100,
|
||||||
},
|
|
||||||
limits: {
|
|
||||||
max: new BigNumber(200),
|
|
||||||
},
|
|
||||||
selectedAsset: undefined,
|
selectedAsset: undefined,
|
||||||
onSelectAsset: jest.fn(),
|
onSelectAsset: jest.fn(),
|
||||||
submitWithdraw: jest.fn().mockReturnValue(Promise.resolve()),
|
submitWithdraw: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
@ -112,6 +108,7 @@ describe('Withdrawal form', () => {
|
|||||||
asset: props.assets[0].id,
|
asset: props.assets[0].id,
|
||||||
amount: '4000000',
|
amount: '4000000',
|
||||||
receiverAddress: MOCK_ETH_ADDRESS,
|
receiverAddress: MOCK_ETH_ADDRESS,
|
||||||
|
availableTimestamp: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -123,7 +120,7 @@ describe('Withdrawal form', () => {
|
|||||||
fireEvent.click(screen.getByText('Use maximum'));
|
fireEvent.click(screen.getByText('Use maximum'));
|
||||||
|
|
||||||
expect(screen.getByLabelText('Amount')).toHaveValue(
|
expect(screen.getByLabelText('Amount')).toHaveValue(
|
||||||
Number(props.max.balance.toFixed(asset.decimals))
|
Number(props.balance.toFixed(asset.decimals))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
t,
|
t,
|
||||||
removeDecimal,
|
removeDecimal,
|
||||||
required,
|
required,
|
||||||
maxSafe,
|
|
||||||
isAssetTypeERC20,
|
isAssetTypeERC20,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import {
|
import {
|
||||||
@ -15,12 +14,11 @@ import {
|
|||||||
InputError,
|
InputError,
|
||||||
Select,
|
Select,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { Web3WalletInput } from '@vegaprotocol/web3';
|
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||||
import type { WithdrawalFields } from './use-withdraw';
|
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||||
import { WithdrawLimits } from './withdraw-limits';
|
import { WithdrawLimits } from './withdraw-limits';
|
||||||
|
|
||||||
interface FormFields {
|
interface FormFields {
|
||||||
@ -31,25 +29,22 @@ interface FormFields {
|
|||||||
|
|
||||||
export interface WithdrawFormProps {
|
export interface WithdrawFormProps {
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
max: {
|
|
||||||
balance: BigNumber;
|
|
||||||
threshold: BigNumber;
|
|
||||||
};
|
|
||||||
min: BigNumber;
|
min: BigNumber;
|
||||||
|
balance: BigNumber;
|
||||||
selectedAsset?: Asset;
|
selectedAsset?: Asset;
|
||||||
limits: {
|
threshold: BigNumber;
|
||||||
max: BigNumber;
|
delay: number | undefined;
|
||||||
} | null;
|
|
||||||
onSelectAsset: (assetId: string) => void;
|
onSelectAsset: (assetId: string) => void;
|
||||||
submitWithdraw: (withdrawal: WithdrawalFields) => void;
|
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithdrawForm = ({
|
export const WithdrawForm = ({
|
||||||
assets,
|
assets,
|
||||||
max,
|
balance,
|
||||||
min,
|
min,
|
||||||
selectedAsset,
|
selectedAsset,
|
||||||
limits,
|
threshold,
|
||||||
|
delay,
|
||||||
onSelectAsset,
|
onSelectAsset,
|
||||||
submitWithdraw,
|
submitWithdraw,
|
||||||
}: WithdrawFormProps) => {
|
}: WithdrawFormProps) => {
|
||||||
@ -67,6 +62,9 @@ export const WithdrawForm = ({
|
|||||||
to: address,
|
to: address,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const amount = useWatch({ name: 'amount', control });
|
||||||
|
|
||||||
const onSubmit = async (fields: FormFields) => {
|
const onSubmit = async (fields: FormFields) => {
|
||||||
if (!selectedAsset) {
|
if (!selectedAsset) {
|
||||||
throw new Error('Asset not selected');
|
throw new Error('Asset not selected');
|
||||||
@ -75,103 +73,117 @@ export const WithdrawForm = ({
|
|||||||
asset: selectedAsset.id,
|
asset: selectedAsset.id,
|
||||||
amount: removeDecimal(fields.amount, selectedAsset.decimals),
|
amount: removeDecimal(fields.amount, selectedAsset.decimals),
|
||||||
receiverAddress: fields.to,
|
receiverAddress: fields.to,
|
||||||
|
availableTimestamp:
|
||||||
|
new BigNumber(fields.amount).isGreaterThan(threshold) && delay
|
||||||
|
? Date.now() + delay * 1000
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<>
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
<div className="mb-4 text-sm">
|
||||||
noValidate={true}
|
<p>{t('There are two steps required to make a withdrawal')}</p>
|
||||||
data-testid="withdraw-form"
|
<ol className="list-disc pl-4">
|
||||||
>
|
<li>{t('Step 1 - Release funds from Vega')}</li>
|
||||||
<FormGroup label={t('Asset')} labelFor="asset">
|
<li>{t('Step 2 - Transfer funds to your Ethereum wallet')}</li>
|
||||||
<Controller
|
</ol>
|
||||||
control={control}
|
</div>
|
||||||
name="asset"
|
<form
|
||||||
rules={{ validate: { required } }}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
render={({ field }) => (
|
noValidate={true}
|
||||||
<Select
|
data-testid="withdraw-form"
|
||||||
{...field}
|
>
|
||||||
onChange={(e) => {
|
<FormGroup label={t('Asset')} labelFor="asset">
|
||||||
onSelectAsset(e.target.value);
|
<Controller
|
||||||
field.onChange(e.target.value);
|
control={control}
|
||||||
}}
|
name="asset"
|
||||||
value={selectedAsset?.id || ''}
|
rules={{ validate: { required } }}
|
||||||
id="asset"
|
render={({ field }) => (
|
||||||
>
|
<Select
|
||||||
<option value="">{t('Please select')}</option>
|
{...field}
|
||||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
onChange={(e) => {
|
||||||
<option key={a.id} value={a.id}>
|
onSelectAsset(e.target.value);
|
||||||
{a.name}
|
field.onChange(e.target.value);
|
||||||
</option>
|
}}
|
||||||
))}
|
value={selectedAsset?.id || ''}
|
||||||
</Select>
|
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>
|
||||||
)}
|
)}
|
||||||
/>
|
</FormGroup>
|
||||||
{errors.asset?.message && (
|
<FormGroup
|
||||||
<InputError intent="danger">{errors.asset.message}</InputError>
|
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('Amount')} labelFor="amount">
|
||||||
<FormGroup label={t('To (Ethereum address)')} labelFor="ethereum-address">
|
<Input
|
||||||
<Web3WalletInput
|
type="number"
|
||||||
inputProps={{
|
autoComplete="off"
|
||||||
id: 'ethereum-address',
|
id="amount"
|
||||||
...register('to', { validate: { required, ethereumAddress } }),
|
{...register('amount', {
|
||||||
}}
|
validate: {
|
||||||
/>
|
required,
|
||||||
{errors.to?.message && (
|
maxSafe: (v) => {
|
||||||
<InputError intent="danger">{errors.to.message}</InputError>
|
const value = new BigNumber(v);
|
||||||
)}
|
if (value.isGreaterThan(balance)) {
|
||||||
</FormGroup>
|
return t('Insufficient amount in account');
|
||||||
{selectedAsset && limits && (
|
}
|
||||||
<div className="mb-6">
|
return true;
|
||||||
<WithdrawLimits limits={limits} balance={max.balance} />
|
},
|
||||||
</div>
|
minSafe: (value) => minSafe(min)(value),
|
||||||
)}
|
|
||||||
<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
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
minSafe: (value) => minSafe(min)(value),
|
})}
|
||||||
},
|
/>
|
||||||
})}
|
{errors.amount?.message && (
|
||||||
/>
|
<InputError intent="danger">{errors.amount.message}</InputError>
|
||||||
{errors.amount?.message && (
|
)}
|
||||||
<InputError intent="danger" className="mt-4">
|
{selectedAsset && (
|
||||||
{errors.amount.message}
|
<UseButton
|
||||||
</InputError>
|
data-testid="use-maximum"
|
||||||
)}
|
onClick={() => {
|
||||||
{selectedAsset && (
|
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||||
<UseButton
|
clearErrors('amount');
|
||||||
data-testid="use-maximum"
|
}}
|
||||||
onClick={() => {
|
>
|
||||||
setValue('amount', max.balance.toFixed(selectedAsset.decimals));
|
{t('Use maximum')}
|
||||||
clearErrors('amount');
|
</UseButton>
|
||||||
}}
|
)}
|
||||||
>
|
</FormGroup>
|
||||||
{t('Use maximum')}
|
<Button data-testid="submit-withdrawal" type="submit" variant="primary">
|
||||||
</UseButton>
|
Release funds
|
||||||
)}
|
</Button>
|
||||||
</FormGroup>
|
</form>
|
||||||
<Button variant="primary" data-testid="submit-withdrawal" type="submit">
|
</>
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,24 +1,35 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import type BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
|
||||||
interface WithdrawLimitsProps {
|
interface WithdrawLimitsProps {
|
||||||
limits: {
|
amount: string;
|
||||||
max: BigNumber;
|
threshold: BigNumber;
|
||||||
};
|
|
||||||
balance: BigNumber;
|
balance: BigNumber;
|
||||||
|
delay: number | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
|
export const WithdrawLimits = ({
|
||||||
let maxLimit = '';
|
amount,
|
||||||
|
threshold,
|
||||||
|
balance,
|
||||||
|
delay,
|
||||||
|
}: WithdrawLimitsProps) => {
|
||||||
|
let text = '';
|
||||||
|
|
||||||
if (limits.max.isEqualTo(Infinity)) {
|
if (threshold.isEqualTo(Infinity)) {
|
||||||
maxLimit = t('No limit');
|
text = t('No limit');
|
||||||
} else if (limits.max.isGreaterThan(1_000_000)) {
|
} else if (threshold.isGreaterThan(1_000_000)) {
|
||||||
maxLimit = t('1m+');
|
text = t('1m+');
|
||||||
} else {
|
} else {
|
||||||
maxLimit = limits.max.toString();
|
text = threshold.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delayTime =
|
||||||
|
new BigNumber(amount).isGreaterThan(threshold) && delay
|
||||||
|
? formatDistanceToNow(Date.now() + delay * 1000)
|
||||||
|
: t('None');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,8 +38,14 @@ export const WithdrawLimits = ({ limits, balance }: WithdrawLimitsProps) => {
|
|||||||
<td className="text-right">{balance.toString()}</td>
|
<td className="text-right">{balance.toString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left font-normal">{t('Maximum withdrawal')}</th>
|
<th className="text-left font-normal">
|
||||||
<td className="text-right">{maxLimit}</td>
|
{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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 { generateAccount, generateAsset } from './test-helpers';
|
||||||
import type { WithdrawManagerProps } from './withdraw-manager';
|
import type { WithdrawManagerProps } from './withdraw-manager';
|
||||||
import { WithdrawManager } 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';
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||||
@ -17,72 +11,46 @@ jest.mock('@web3-react/core', () => ({
|
|||||||
useWeb3React: () => ({ account: ethereumAddress }),
|
useWeb3React: () => ({ account: ethereumAddress }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./use-get-withdraw-limits', () => ({
|
jest.mock('./use-get-withdraw-threshold', () => ({
|
||||||
useGetWithdrawLimits: () => {
|
useGetWithdrawThreshold: () => {
|
||||||
return { max: new BigNumber(1000000) };
|
return () => Promise.resolve(new BigNumber(100));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./use-get-withdraw-delay', () => ({
|
||||||
|
useGetWithdrawDelay: () => {
|
||||||
|
return () => Promise.resolve(10000);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let props: WithdrawManagerProps;
|
let props: WithdrawManagerProps;
|
||||||
let useWithdrawValue: ReturnType<typeof withdrawHook.useWithdraw>;
|
|
||||||
let useWithdraw: jest.SpyInstance;
|
|
||||||
let mockSubmit: jest.Mock;
|
|
||||||
let mockReset: jest.Mock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
assets: [generateAsset()],
|
assets: [generateAsset()],
|
||||||
accounts: [generateAccount()],
|
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) => (
|
const generateJsx = (props: WithdrawManagerProps) => (
|
||||||
<WithdrawManager {...props} />
|
<WithdrawManager {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
it('Valid form submission opens transaction dialog', async () => {
|
it('calls submit if valid form submission', async () => {
|
||||||
render(generateJsx(props));
|
render(generateJsx(props));
|
||||||
submitValid();
|
await act(async () => {
|
||||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
await submitValid();
|
||||||
expect(mockReset).toHaveBeenCalled();
|
});
|
||||||
expect(mockSubmit).toHaveBeenCalledWith({
|
expect(props.submit).toHaveBeenCalledWith({
|
||||||
amount: '1000',
|
amount: '1000',
|
||||||
asset: props.assets[0].id,
|
asset: props.assets[0].id,
|
||||||
receiverAddress: ethereumAddress,
|
receiverAddress: ethereumAddress,
|
||||||
|
availableTimestamp: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Expected Ethereum error closes the dialog', async () => {
|
it('validates correctly', 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 () => {
|
|
||||||
render(generateJsx(props));
|
render(generateJsx(props));
|
||||||
|
|
||||||
// Set other fields to be valid
|
// 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'));
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
||||||
expect(mockSubmit).not.toBeCalled();
|
expect(props.submit).not.toBeCalled();
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
target: { value: '0.00001' },
|
target: { value: '0.00001' },
|
||||||
@ -113,19 +81,14 @@ it('Correct min max values provided to form', async () => {
|
|||||||
expect(
|
expect(
|
||||||
await screen.findByText('Insufficient amount in account')
|
await screen.findByText('Insufficient amount in account')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(mockSubmit).not.toBeCalled();
|
expect(props.submit).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Initial asset id can preselect asset', async () => {
|
const submitValid = async () => {
|
||||||
const asset = props.assets[0];
|
await userEvent.selectOptions(
|
||||||
render(generateJsx({ ...props, initialAssetId: asset.id }));
|
screen.getByLabelText('Asset'),
|
||||||
expect(screen.getByLabelText('Asset')).toHaveValue(asset.id);
|
props.assets[0].id
|
||||||
});
|
);
|
||||||
|
|
||||||
const submitValid = () => {
|
|
||||||
fireEvent.change(screen.getByLabelText('Asset'), {
|
|
||||||
target: { value: props.assets[0].id },
|
|
||||||
});
|
|
||||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||||
target: { value: ethereumAddress },
|
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 sortBy from 'lodash/sortBy';
|
||||||
import { WithdrawForm } from './withdraw-form';
|
import { WithdrawForm } from './withdraw-form';
|
||||||
import type { WithdrawalFields } from './use-withdraw';
|
import type { WithdrawalArgs } from './use-create-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 { addDecimal } from '@vegaprotocol/react-helpers';
|
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import type { Account } from './types';
|
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 {
|
export interface WithdrawManagerProps {
|
||||||
assets: Asset[];
|
assets: AssetFields[];
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
initialAssetId?: string;
|
submit: (args: WithdrawalArgs) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithdrawManager = ({
|
export const WithdrawManager = ({
|
||||||
assets,
|
assets,
|
||||||
accounts,
|
accounts,
|
||||||
initialAssetId,
|
submit,
|
||||||
}: WithdrawManagerProps) => {
|
}: WithdrawManagerProps) => {
|
||||||
const dialogDismissed = useRef(false);
|
const { asset, balance, min, threshold, delay, update } = useWithdrawStore();
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const getThreshold = useGetWithdrawThreshold();
|
||||||
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
|
const getDelay = useGetWithdrawDelay();
|
||||||
|
|
||||||
const { ethTx, vegaTx, approval, submit, reset } = useWithdraw(
|
// Everytime an asset is selected we need to find the corresponding
|
||||||
dialogDismissed.current
|
// 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
|
// Query collateral bridge for threshold for selected asset
|
||||||
const asset = useMemo(() => {
|
// and subsequent delay if withdrawal amount is larger than it
|
||||||
return assets?.find((a) => a.id === assetId);
|
let threshold;
|
||||||
}, [assets, assetId]);
|
let delay;
|
||||||
|
|
||||||
const account = useMemo(() => {
|
try {
|
||||||
return accounts.find(
|
const result = await Promise.all([getThreshold(asset), getDelay()]);
|
||||||
(a) =>
|
threshold = result[0];
|
||||||
a.type === AccountType.ACCOUNT_TYPE_GENERAL && a.asset.id === asset?.id
|
delay = result[1];
|
||||||
);
|
} catch (err) {
|
||||||
}, [asset, accounts]);
|
captureException(err);
|
||||||
|
}
|
||||||
|
|
||||||
const limits = useGetWithdrawLimits(asset);
|
update({ asset, balance, min, threshold, delay });
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
[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 (
|
return (
|
||||||
<>
|
<WithdrawForm
|
||||||
<WithdrawForm
|
selectedAsset={asset}
|
||||||
selectedAsset={asset}
|
onSelectAsset={handleSelectAsset}
|
||||||
onSelectAsset={(id) => setAssetId(id)}
|
assets={sortBy(assets, 'name')}
|
||||||
assets={sortBy(assets, 'name')}
|
balance={balance}
|
||||||
max={max}
|
min={min}
|
||||||
min={min}
|
submitWithdraw={submit}
|
||||||
submitWithdraw={handleSubmit}
|
threshold={threshold}
|
||||||
limits={limits}
|
delay={delay}
|
||||||
/>
|
/>
|
||||||
<WithdrawDialog
|
|
||||||
vegaTx={vegaTx}
|
|
||||||
ethTx={ethTx}
|
|
||||||
approval={approval}
|
|
||||||
dialogOpen={dialogOpen}
|
|
||||||
onDialogChange={(isOpen) => {
|
|
||||||
setDialogOpen(isOpen);
|
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
dialogDismissed.current = true;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
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,
|
addDecimalsFormatNumber,
|
||||||
getDateTimeFormat,
|
getDateTimeFormat,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
import { WithdrawalStatus, WithdrawalStatusMapping } from '@vegaprotocol/types';
|
||||||
import { generateWithdrawal } from './test-helpers';
|
import { generateWithdrawal } from './test-helpers';
|
||||||
import type {
|
import type {
|
||||||
StatusCellProps,
|
StatusCellProps,
|
||||||
@ -12,7 +12,7 @@ import type {
|
|||||||
} from './withdrawals-table';
|
} from './withdrawals-table';
|
||||||
import { StatusCell } from './withdrawals-table';
|
import { StatusCell } from './withdrawals-table';
|
||||||
import { WithdrawalsTable } 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', () => ({
|
jest.mock('@web3-react/core', () => ({
|
||||||
useWeb3React: () => ({ provider: undefined }),
|
useWeb3React: () => ({ provider: undefined }),
|
||||||
@ -24,38 +24,66 @@ const generateJsx = (props: WithdrawalsTableProps) => (
|
|||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
it('Renders the correct columns', async () => {
|
describe('renders the correct columns', () => {
|
||||||
const withdrawal = generateWithdrawal();
|
it('incomplete withdrawal', async () => {
|
||||||
await act(async () => {
|
const withdrawal = generateWithdrawal();
|
||||||
render(generateJsx({ withdrawals: [withdrawal] }));
|
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');
|
it('completed withdrawal', async () => {
|
||||||
expect(headers).toHaveLength(5);
|
const withdrawal = generateWithdrawal({
|
||||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
txHash: '0x1234567891011121314',
|
||||||
'Asset',
|
status: WithdrawalStatus.STATUS_FINALIZED,
|
||||||
'Amount',
|
});
|
||||||
'Recipient',
|
|
||||||
'Created at',
|
|
||||||
'Status',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cells = screen.getAllByRole('gridcell');
|
await act(async () => {
|
||||||
const expectedValues = [
|
render(generateJsx({ withdrawals: [withdrawal] }));
|
||||||
'asset-symbol',
|
});
|
||||||
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
|
||||||
'123456\u2026123456',
|
const cells = screen.getAllByRole('gridcell');
|
||||||
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
const expectedValues = [
|
||||||
withdrawal.status,
|
'asset-symbol',
|
||||||
];
|
addDecimalsFormatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
||||||
cells.forEach((cell, i) => {
|
'123456…123456',
|
||||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||||
|
'0x1234…121314',
|
||||||
|
WithdrawalStatusMapping[withdrawal.status],
|
||||||
|
];
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('StatusCell', () => {
|
describe('StatusCell', () => {
|
||||||
let props: StatusCellProps;
|
let props: StatusCellProps;
|
||||||
let withdrawal: Withdrawals_party_withdrawals;
|
let withdrawal: Withdrawals_party_withdrawalsConnection_edges_node;
|
||||||
let mockComplete: jest.Mock;
|
let mockComplete: jest.Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -76,9 +104,7 @@ describe('StatusCell', () => {
|
|||||||
render(<StatusCell {...props} />);
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||||
fireEvent.click(
|
fireEvent.click(screen.getByText('Complete', { selector: 'button' }));
|
||||||
screen.getByText('Click to complete', { selector: 'button' })
|
|
||||||
);
|
|
||||||
expect(mockComplete).toHaveBeenCalled();
|
expect(mockComplete).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -102,18 +128,5 @@ describe('StatusCell', () => {
|
|||||||
render(<StatusCell {...props} />);
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
expect(screen.getByText('Finalized')).toBeInTheDocument();
|
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 { AgGridColumn } from 'ag-grid-react';
|
||||||
import {
|
import {
|
||||||
getDateTimeFormat,
|
getDateTimeFormat,
|
||||||
@ -9,19 +5,40 @@ import {
|
|||||||
truncateByChars,
|
truncateByChars,
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { WithdrawalStatus } from '@vegaprotocol/types';
|
import type {
|
||||||
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
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 { useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { useCompleteWithdraw } from './use-complete-withdraw';
|
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 {
|
export interface WithdrawalsTableProps {
|
||||||
withdrawals: Withdrawals_party_withdrawals[];
|
withdrawals: WithdrawalFields[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
||||||
const { ETHERSCAN_URL } = useEnvironment();
|
const { ETHERSCAN_URL } = useEnvironment();
|
||||||
const { submit, Dialog } = useCompleteWithdraw();
|
const {
|
||||||
|
submit,
|
||||||
|
reset: resetTx,
|
||||||
|
Dialog: EthereumTransactionDialog,
|
||||||
|
} = useCompleteWithdraw();
|
||||||
|
const {
|
||||||
|
verify,
|
||||||
|
state: verifyState,
|
||||||
|
reset: resetVerification,
|
||||||
|
} = useVerifyWithdrawal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -35,51 +52,105 @@ export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
|||||||
>
|
>
|
||||||
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName="Amount"
|
headerName={t('Amount')}
|
||||||
field="amount"
|
field="amount"
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: VegaValueFormatterParams<WithdrawalFields, 'amount'>) => {
|
||||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName="Recipient"
|
headerName={t('Recipient')}
|
||||||
field="details.receiverAddress"
|
field="details.receiverAddress"
|
||||||
cellRenderer="RecipientCell"
|
cellRenderer="RecipientCell"
|
||||||
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
|
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
WithdrawalFields,
|
||||||
|
'details.receiverAddress'
|
||||||
|
>) => {
|
||||||
|
if (!value) return '-';
|
||||||
return truncateByChars(value);
|
return truncateByChars(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName="Created at"
|
headerName={t('Created at')}
|
||||||
field="createdTimestamp"
|
field="createdTimestamp"
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
WithdrawalFields,
|
||||||
|
'createdTimestamp'
|
||||||
|
>) => {
|
||||||
return getDateTimeFormat().format(new Date(value));
|
return getDateTimeFormat().format(new Date(value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<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"
|
field="status"
|
||||||
cellRenderer="StatusCell"
|
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>
|
</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;
|
ethUrl: string;
|
||||||
complete: (withdrawalId: string) => void;
|
complete: (withdrawal: WithdrawalFields) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusCell = ({
|
export const StatusCell = ({ ethUrl, data, complete }: StatusCellProps) => {
|
||||||
ethUrl,
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
complete,
|
|
||||||
}: StatusCellProps) => {
|
|
||||||
if (data.pendingOnForeignChain) {
|
if (data.pendingOnForeignChain) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-8">
|
<div className="flex justify-between gap-8">
|
||||||
@ -98,37 +169,22 @@ export const StatusCell = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === WithdrawalStatus.STATUS_FINALIZED) {
|
if (!data.txHash) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-8">
|
<div className="flex justify-between gap-8">
|
||||||
{data.txHash ? (
|
{t('Open')}
|
||||||
<>
|
<button className="underline" onClick={() => complete(data)}>
|
||||||
{t('Finalized')}
|
{t('Complete')}
|
||||||
<Link
|
</button>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return <span>{t('Finalized')}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RecipientCellProps extends ICellRendererParams {
|
export interface RecipientCellProps
|
||||||
|
extends VegaICellRendererParams<WithdrawalFields, 'details.receiverAddress'> {
|
||||||
ethUrl: string;
|
ethUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,3 +204,48 @@ const RecipientCell = ({
|
|||||||
</Link>
|
</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",
|
"**/*.test.jsx",
|
||||||
"**/*.spec.jsx",
|
"**/*.spec.jsx",
|
||||||
"**/*.d.ts",
|
"**/*.d.ts",
|
||||||
"src/lib/withdraw-manager.foo.tsx",
|
|
||||||
"jest.config.ts"
|
"jest.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user