Feat/104 withdraws (#263)
* add withdraw page * add create page and withdraws lib with basic components * add create withdrawal functionality, add withdrawal dialog * re arrange pages so we have a pending page with a list of withdrawals and a withdraw page with the form * handle withdrawal completion * add withdraw dialog so withdraw is completed in one flow * rename and arrange prop getter function to avoid use of any * tidy up and make eth transaction and vega transaction hooks follow similar patterns * make complete withdraw hook fetch approval, combine hooks for full flow * delete unused file * add accounts to query to get use max withdraw button * use ag grid fro withdrawals list * format amount in grid * ensure party has id field for cache * update to use t i18n function * change complete status to better match values of WithdrawalStatus * add asset lib file for type guard * share queries for withdraws lib * move web3 related hooks to the web3 lib and make web3 lib buildable * add unit test for use-complete-withdraw * add test for use-withdraw * add e2e scenarios for withdrawals * don render custom no withdrawals message and use ag grid prop * make withdraw form take ethereum account as prop * add test config, update withdraw form to handle min/max amounts, add unit test for withdraw form * fix rendering 0 if not pending withdrawals * use controlled component for select box * tidy up async parts of tests, add test for withdraw-manager * update withdraw dialog to use map for different tx states * add fallback etherscan link, add test for withdraw-dialog.spec.tsx * rename erc20 query in token app, change page structure to /withdraw and /withdrawals * change linked vegawallet-service-api-client to newly published version * ignore not implemented withdrawals e2e tests * make vega tx pending and requested states the same, for now, until tx confirmation work is done * Add some unit tests for determine-ids and remove0x Neither are critical, but both are libs expected to be shared between projects so some unit tests will hopefully ensure projects all have the same expectations about how the functions act. - Add 1 determine-ids test - Add a few remove-0x tests - Update determine-ids to use remove-0x - Add a comment describing why i18n is what it is * modify cache with client only field for tracking foreign chain progress * move withdrawals-list to lib, add useWithdrawals hook * add container component for withdrawals page * add unit tests for withdrawals-table * add some tests for use-withdrawals hook * revert changes to generated files in other libs and apps * update withdrawals scenarios * resolve pr comments * update page containers to use render prop rather than children * delete unused files * remove transaction dialog story Co-authored-by: Ditmir-Vega <ditmir@vega.xyz> Co-authored-by: Edd <edd@vega.xyz>
This commit is contained in:
parent
5134df02b5
commit
29cd7dd2e4
48
apps/token/src/hooks/__generated__/Erc20ApprovalPoll.ts
generated
Normal file
48
apps/token/src/hooks/__generated__/Erc20ApprovalPoll.ts
generated
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: Erc20ApprovalPoll
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Erc20ApprovalPoll_erc20WithdrawalApproval {
|
||||||
|
__typename: "Erc20WithdrawalApproval";
|
||||||
|
/**
|
||||||
|
* The source asset in the ethereum network
|
||||||
|
*/
|
||||||
|
assetSource: string;
|
||||||
|
/**
|
||||||
|
* The amount to be withdrawn
|
||||||
|
*/
|
||||||
|
amount: string;
|
||||||
|
/**
|
||||||
|
* The nonce to be used in the request
|
||||||
|
*/
|
||||||
|
nonce: string;
|
||||||
|
/**
|
||||||
|
* Signature aggregate from the nodes, in the following format:
|
||||||
|
* 0x + sig1 + sig2 + ... + sigN
|
||||||
|
*/
|
||||||
|
signatures: string;
|
||||||
|
/**
|
||||||
|
* The target address which will receive the funds
|
||||||
|
*/
|
||||||
|
targetAddress: string;
|
||||||
|
/**
|
||||||
|
* Timestamp in seconds for expiry of the approval
|
||||||
|
*/
|
||||||
|
expiry: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Erc20ApprovalPoll {
|
||||||
|
/**
|
||||||
|
* find an erc20 withdrawal approval using its withdrawal id
|
||||||
|
*/
|
||||||
|
erc20WithdrawalApproval: Erc20ApprovalPoll_erc20WithdrawalApproval | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Erc20ApprovalPollVariables {
|
||||||
|
withdrawalId: string;
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { WithdrawalStatus } from "./../../__generated__/globalTypes";
|
import { WithdrawalStatus } from "./../../../../../libs/types/src/__generated__/globalTypes";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: WithdrawalPoll
|
// GraphQL query operation: WithdrawalPoll
|
||||||
|
@ -2,13 +2,13 @@ import React from 'react';
|
|||||||
import { gql, useApolloClient } from '@apollo/client';
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Erc20Approval,
|
Erc20ApprovalPoll,
|
||||||
Erc20Approval_erc20WithdrawalApproval,
|
Erc20ApprovalPoll_erc20WithdrawalApproval,
|
||||||
Erc20ApprovalVariables,
|
Erc20ApprovalPollVariables,
|
||||||
} from './__generated__/Erc20Approval';
|
} from './__generated__/Erc20ApprovalPoll';
|
||||||
|
|
||||||
const ERC20_APPROVAL_QUERY = gql`
|
const ERC20_APPROVAL_QUERY = gql`
|
||||||
query Erc20Approval($withdrawalId: ID!) {
|
query Erc20ApprovalPoll($withdrawalId: ID!) {
|
||||||
erc20WithdrawalApproval(withdrawalId: $withdrawalId) {
|
erc20WithdrawalApproval(withdrawalId: $withdrawalId) {
|
||||||
assetSource
|
assetSource
|
||||||
amount
|
amount
|
||||||
@ -24,10 +24,10 @@ export const usePollERC20Approval = (withdrawalId: string) => {
|
|||||||
const mountedRef = React.useRef(true);
|
const mountedRef = React.useRef(true);
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [erc20Approval, setErc20Approval] =
|
const [erc20Approval, setErc20Approval] =
|
||||||
React.useState<Erc20Approval_erc20WithdrawalApproval | null>(null);
|
React.useState<Erc20ApprovalPoll_erc20WithdrawalApproval | null>(null);
|
||||||
|
|
||||||
const safeSetErc20Approval = (
|
const safeSetErc20Approval = (
|
||||||
approval: Erc20Approval_erc20WithdrawalApproval
|
approval: Erc20ApprovalPoll_erc20WithdrawalApproval
|
||||||
) => {
|
) => {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setErc20Approval(approval);
|
setErc20Approval(approval);
|
||||||
@ -37,7 +37,10 @@ export const usePollERC20Approval = (withdrawalId: string) => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await client.query<Erc20Approval, Erc20ApprovalVariables>({
|
const res = await client.query<
|
||||||
|
Erc20ApprovalPoll,
|
||||||
|
Erc20ApprovalPollVariables
|
||||||
|
>({
|
||||||
query: ERC20_APPROVAL_QUERY,
|
query: ERC20_APPROVAL_QUERY,
|
||||||
variables: { withdrawalId },
|
variables: { withdrawalId },
|
||||||
});
|
});
|
||||||
|
76
apps/trading-e2e/src/integration/withdrawals.feature
Normal file
76
apps/trading-e2e/src/integration/withdrawals.feature
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
@ignore
|
||||||
|
Feature: Withdrawals
|
||||||
|
|
||||||
|
Scenario: Can prepare a withdrawal
|
||||||
|
Given I am on withdraw page
|
||||||
|
And I connect eth wallet
|
||||||
|
And I connect vega wallet
|
||||||
|
And I can see the withdrawals warning info
|
||||||
|
And I can see the from ethereum address field is automatically populated with current connected key
|
||||||
|
And I select asset 'tBTC'
|
||||||
|
And I input '100' tokens to withdraw
|
||||||
|
And the withdraw button is enabled
|
||||||
|
When I click the withdraw button
|
||||||
|
Then The vega transaction is sent
|
||||||
|
Then The Ethereum transaction is triggered
|
||||||
|
|
||||||
|
Scenario: Form field validation if fields are incomplete
|
||||||
|
Given I am on withdraw page
|
||||||
|
And I connect eth wallet
|
||||||
|
And I connect vega wallet
|
||||||
|
When I click the withdraw button
|
||||||
|
Then I can see validation errors on incomplete fields
|
||||||
|
|
||||||
|
Scenario: Can prepare a withdrawal to send to another eth wallet
|
||||||
|
Given I am on withdraw page
|
||||||
|
And I connect eth wallet
|
||||||
|
And I connect vega wallet
|
||||||
|
And I select asset 'tBTC'
|
||||||
|
And I can see an eth address is already filled in the withdraw to field
|
||||||
|
When I click the enter manually button
|
||||||
|
Then I can enter a new eth address
|
||||||
|
And I input '100' tokens to withdraw
|
||||||
|
And the withdraw button is enabled
|
||||||
|
When I click the withdraw button
|
||||||
|
Then The vega transaction is sent
|
||||||
|
Then The Ethereum transaction is triggered
|
||||||
|
|
||||||
|
Scenario: Eth key validation on form
|
||||||
|
Given I am on withdraw page
|
||||||
|
And I connect eth wallet
|
||||||
|
And I connect vega wallet
|
||||||
|
And I select asset 'tBTC'
|
||||||
|
When I click the enter manually button
|
||||||
|
And I enter eth address 'MMMMNNNN'
|
||||||
|
Then the invalid eth address error is shown
|
||||||
|
|
||||||
|
Scenario: Validation error if trying to withdraw more than available
|
||||||
|
Given I am on withdrawals page
|
||||||
|
And I have connected
|
||||||
|
And I select asset 'tBTC'
|
||||||
|
And I input '1088494949494949940' tokens to withdraw
|
||||||
|
When I click the withdraw button
|
||||||
|
Then validation error is shown for token input amount
|
||||||
|
|
||||||
|
Scenario: Can see pending / unfinished withdrawals
|
||||||
|
Given I am on the withdrawals page
|
||||||
|
And I can see there are unfinished withdrawals
|
||||||
|
And I can see the complete withdrawals button
|
||||||
|
|
||||||
|
# Needs capsule
|
||||||
|
@manual
|
||||||
|
Scenario: Finish withdrawal to eth wallet
|
||||||
|
Given I am on the withdrawals page
|
||||||
|
And I can see there are unfinished withdrawals
|
||||||
|
And I click on an unfinished withdrawal button
|
||||||
|
Then I approve transaction on ethereum
|
||||||
|
Then I can see the withdrawal button state has changed to pending
|
||||||
|
When The transaction is complete
|
||||||
|
Then My balance has been updated
|
||||||
|
|
||||||
|
@manual
|
||||||
|
Scenario: Withdrawals after chain reset
|
||||||
|
Given I am on the withdrawals page
|
||||||
|
And I previously had withdrawals
|
||||||
|
And There has been a chain reset
|
||||||
|
Then There should be no incomplete withdrawals
|
4
apps/trading/client.graphql
Normal file
4
apps/trading/client.graphql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
extend type Withdrawal {
|
||||||
|
"Whether or the not the withdrawal is being processed on Ethereum"
|
||||||
|
pendingOnForeignChain: Boolean!
|
||||||
|
}
|
@ -50,14 +50,14 @@ jest.mock('@web3-react/core', () => {
|
|||||||
function setup(mock = networkParamsQueryMock) {
|
function setup(mock = networkParamsQueryMock) {
|
||||||
return render(
|
return render(
|
||||||
<MockedProvider mocks={[mock]}>
|
<MockedProvider mocks={[mock]}>
|
||||||
<Web3Container>
|
<Web3Container
|
||||||
{({ ethereumConfig }) => (
|
render={({ ethereumConfig }) => (
|
||||||
<div>
|
<div>
|
||||||
<div>Child</div>
|
<div>Child</div>
|
||||||
<div>{ethereumConfig.collateral_bridge_contract.address}</div>
|
<div>{ethereumConfig.collateral_bridge_contract.address}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Web3Container>
|
/>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,10 @@ export const NETWORK_PARAMS_QUERY = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface Web3ContainerProps {
|
interface Web3ContainerProps {
|
||||||
children: (params: { ethereumConfig: EthereumConfig }) => ReactNode;
|
render: (params: { ethereumConfig: EthereumConfig }) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Web3Container = ({ children }: Web3ContainerProps) => {
|
export const Web3Container = ({ render }: Web3ContainerProps) => {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<PageQueryContainer<NetworkParamsQuery>
|
<PageQueryContainer<NetworkParamsQuery>
|
||||||
@ -79,7 +79,7 @@ export const Web3Container = ({ children }: Web3ContainerProps) => {
|
|||||||
appChainId={Number(ethereumConfig.chain_id)}
|
appChainId={Number(ethereumConfig.chain_id)}
|
||||||
setDialogOpen={setDialogOpen}
|
setDialogOpen={setDialogOpen}
|
||||||
>
|
>
|
||||||
{children({ ethereumConfig })}
|
{render({ ethereumConfig })}
|
||||||
</Web3Content>
|
</Web3Content>
|
||||||
<Web3ConnectDialog
|
<Web3ConnectDialog
|
||||||
connectors={Connectors}
|
connectors={Connectors}
|
||||||
|
46
apps/trading/lib/__generated__/AssetFields.ts
generated
Normal file
46
apps/trading/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
|
||||||
|
*/
|
||||||
|
decimals: number;
|
||||||
|
/**
|
||||||
|
* The origin source of the asset (e.g: an erc20 asset)
|
||||||
|
*/
|
||||||
|
source: AssetFields_source;
|
||||||
|
}
|
@ -39,6 +39,13 @@ export function createClient(base?: string) {
|
|||||||
Node: {
|
Node: {
|
||||||
keyFields: false,
|
keyFields: false,
|
||||||
},
|
},
|
||||||
|
Withdrawal: {
|
||||||
|
fields: {
|
||||||
|
pendingOnForeignChain: {
|
||||||
|
read: (isPending = false) => isPending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
16
apps/trading/lib/assets.ts
Normal file
16
apps/trading/lib/assets.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { AssetFields } from './__generated__/AssetFields';
|
||||||
|
|
||||||
|
export interface ERC20Asset extends AssetFields {
|
||||||
|
source: {
|
||||||
|
__typename: 'ERC20';
|
||||||
|
contractAddress: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to ensure an asset is an ERC20 token
|
||||||
|
export const isERC20Asset = (asset: AssetFields): asset is ERC20Asset => {
|
||||||
|
if (asset.source.__typename === 'ERC20') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
15
apps/trading/lib/query-fragments.ts
Normal file
15
apps/trading/lib/query-fragments.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const ASSET_FRAGMENT = gql`
|
||||||
|
fragment AssetFields on Asset {
|
||||||
|
id
|
||||||
|
symbol
|
||||||
|
name
|
||||||
|
decimals
|
||||||
|
source {
|
||||||
|
... on ERC20 {
|
||||||
|
contractAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -1,27 +1,18 @@
|
|||||||
import type { EthereumConfig } from '../../../components/web3-container/web3-container';
|
import type { EthereumConfig } from '../../../components/web3-container/web3-container';
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { PageQueryContainer } from '../../../components/page-query-container';
|
import { PageQueryContainer } from '../../../components/page-query-container';
|
||||||
import type {
|
import type { DepositPage } from './__generated__/DepositPage';
|
||||||
DepositPage,
|
|
||||||
DepositPage_assets,
|
|
||||||
} from './__generated__/DepositPage';
|
|
||||||
import type { Asset } from '@vegaprotocol/deposits';
|
|
||||||
import { DepositManager } from '@vegaprotocol/deposits';
|
import { DepositManager } from '@vegaprotocol/deposits';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ASSET_FRAGMENT } from '../../../lib/query-fragments';
|
||||||
|
import { isERC20Asset } from '../../../lib/assets';
|
||||||
|
|
||||||
const DEPOSIT_PAGE_QUERY = gql`
|
const DEPOSIT_PAGE_QUERY = gql`
|
||||||
|
${ASSET_FRAGMENT}
|
||||||
query DepositPage {
|
query DepositPage {
|
||||||
assets {
|
assets {
|
||||||
id
|
...AssetFields
|
||||||
symbol
|
|
||||||
name
|
|
||||||
decimals
|
|
||||||
source {
|
|
||||||
... on ERC20 {
|
|
||||||
contractAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -62,10 +53,3 @@ export const DepositContainer = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isERC20Asset = (asset: DepositPage_assets): asset is Asset => {
|
|
||||||
if (asset.source.__typename === 'ERC20') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
@ -20,14 +20,14 @@ const Deposit = () => {
|
|||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Web3Container>
|
<Web3Container
|
||||||
{({ ethereumConfig }) => (
|
render={({ ethereumConfig }) => (
|
||||||
<div className="max-w-[420px] p-24 mx-auto">
|
<div className="max-w-[420px] p-24 mx-auto">
|
||||||
<h1 className="text-h3 mb-12">Deposit</h1>
|
<h1 className="text-h3 mb-12">Deposit</h1>
|
||||||
<DepositContainer ethereumConfig={ethereumConfig} assetId={assetId} />
|
<DepositContainer ethereumConfig={ethereumConfig} assetId={assetId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Web3Container>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
|
||||||
|
|
||||||
const Portfolio = () => {
|
const Portfolio = () => {
|
||||||
const { keypair } = useVegaWallet();
|
|
||||||
return (
|
return (
|
||||||
<div className="p-24">
|
<div className="p-24">
|
||||||
<h1 className="text-h3 mb-12">{t('Portfolio')}</h1>
|
<h1 className="text-h3 mb-12">{t('Portfolio')}</h1>
|
||||||
{keypair && <p>{t(`Keypair: ${keypair.name} ${keypair.pub}`)}</p>}
|
<div className="flex gap-4 mb-12">
|
||||||
<div className="flex gap-4">
|
|
||||||
<AnchorButton href="/portfolio/deposit">{t('Deposit')}</AnchorButton>
|
<AnchorButton href="/portfolio/deposit">{t('Deposit')}</AnchorButton>
|
||||||
<AnchorButton href="/portfolio/deposit?assetId=8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4">
|
<AnchorButton href="/portfolio/deposit?assetId=8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4">
|
||||||
{t('Deposit tEURO')}
|
{t('Deposit tEURO')}
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<AnchorButton href="/portfolio/withdrawals">
|
||||||
|
{t('View Withdrawals')}
|
||||||
|
</AnchorButton>
|
||||||
|
<AnchorButton href="/portfolio/withdraw">{t('Withdraw')}</AnchorButton>
|
||||||
|
<AnchorButton href="/portfolio/withdraw?assetId=8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4">
|
||||||
|
{t('Withdraw tEURO')}
|
||||||
|
</AnchorButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
119
apps/trading/pages/portfolio/withdraw/__generated__/WithdrawPageQuery.ts
generated
Normal file
119
apps/trading/pages/portfolio/withdraw/__generated__/WithdrawPageQuery.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 "./../../../../../../libs/types/src/__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: WithdrawPageQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_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 WithdrawPageQuery_party_accounts_asset {
|
||||||
|
__typename: "Asset";
|
||||||
|
/**
|
||||||
|
* The id of the asset
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The symbol of the asset (e.g: GBP)
|
||||||
|
*/
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_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: WithdrawPageQuery_party_accounts_asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_party {
|
||||||
|
__typename: "Party";
|
||||||
|
/**
|
||||||
|
* Party identifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The list of all withdrawals initiated by the party
|
||||||
|
*/
|
||||||
|
withdrawals: WithdrawPageQuery_party_withdrawals[] | null;
|
||||||
|
/**
|
||||||
|
* Collateral accounts relating to a party
|
||||||
|
*/
|
||||||
|
accounts: WithdrawPageQuery_party_accounts[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_assets_source_BuiltinAsset {
|
||||||
|
__typename: "BuiltinAsset";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_assets_source_ERC20 {
|
||||||
|
__typename: "ERC20";
|
||||||
|
/**
|
||||||
|
* The address of the erc20 contract
|
||||||
|
*/
|
||||||
|
contractAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WithdrawPageQuery_assets_source = WithdrawPageQuery_assets_source_BuiltinAsset | WithdrawPageQuery_assets_source_ERC20;
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery_assets {
|
||||||
|
__typename: "Asset";
|
||||||
|
/**
|
||||||
|
* The id of the asset
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The symbol of the asset (e.g: GBP)
|
||||||
|
*/
|
||||||
|
symbol: string;
|
||||||
|
/**
|
||||||
|
* The full name of the asset (e.g: Great British Pound)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* The precision of the asset
|
||||||
|
*/
|
||||||
|
decimals: number;
|
||||||
|
/**
|
||||||
|
* The origin source of the asset (e.g: an erc20 asset)
|
||||||
|
*/
|
||||||
|
source: WithdrawPageQuery_assets_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQuery {
|
||||||
|
/**
|
||||||
|
* An entity that is trading on the VEGA network
|
||||||
|
*/
|
||||||
|
party: WithdrawPageQuery_party | null;
|
||||||
|
/**
|
||||||
|
* The list of all assets in use in the vega network
|
||||||
|
*/
|
||||||
|
assets: WithdrawPageQuery_assets[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawPageQueryVariables {
|
||||||
|
partyId: string;
|
||||||
|
}
|
35
apps/trading/pages/portfolio/withdraw/index.page.tsx
Normal file
35
apps/trading/pages/portfolio/withdraw/index.page.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Web3Container } from '../../../components/web3-container';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { WithdrawPageContainer } from './withdraw-page-container';
|
||||||
|
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 (
|
||||||
|
<Web3Container
|
||||||
|
render={() => (
|
||||||
|
<div className="max-w-[420px] p-24 mx-auto">
|
||||||
|
<h1 className="text-h3 mb-12">{t('Withdraw')}</h1>
|
||||||
|
<WithdrawPageContainer assetId={assetId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Withdraw;
|
@ -0,0 +1,97 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { 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 { isERC20Asset } from '../../../lib/assets';
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assets {
|
||||||
|
...AssetFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface WithdrawPageContainerProps {
|
||||||
|
assetId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data required for the Deposit page
|
||||||
|
*/
|
||||||
|
export const WithdrawPageContainer = ({
|
||||||
|
assetId,
|
||||||
|
}: WithdrawPageContainerProps) => {
|
||||||
|
const { keypair } = useVegaWallet();
|
||||||
|
|
||||||
|
if (!keypair) {
|
||||||
|
return <p>{t('Please connect your Vega wallet')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageQueryContainer<WithdrawPageQuery, WithdrawPageQueryVariables>
|
||||||
|
query={WITHDRAW_PAGE_QUERY}
|
||||||
|
options={{
|
||||||
|
variables: { partyId: keypair?.pub || '' },
|
||||||
|
skip: !keypair?.pub,
|
||||||
|
}}
|
||||||
|
render={(data) => {
|
||||||
|
if (!data.assets?.length) {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<p>{t('No assets on this network')}</p>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasIncompleteWithdrawals = data.party?.withdrawals?.some(
|
||||||
|
(w) => w.txHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasIncompleteWithdrawals ? (
|
||||||
|
<p className="mb-12">
|
||||||
|
{t('You have incomplete withdrawals.')}{' '}
|
||||||
|
<Link href="/portfolio/withdrawals">
|
||||||
|
<a className="underline">
|
||||||
|
{t('Click here to finish withdrawal')}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<WithdrawManager
|
||||||
|
assets={data.assets.filter(isERC20Asset)}
|
||||||
|
accounts={data.party?.accounts || []}
|
||||||
|
initialAssetId={assetId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
31
apps/trading/pages/portfolio/withdrawals/index.page.tsx
Normal file
31
apps/trading/pages/portfolio/withdrawals/index.page.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { AnchorButton, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { Web3Container } from '../../../components/web3-container';
|
||||||
|
import { WithdrawalsPageContainer } from './withdrawals-page-container';
|
||||||
|
|
||||||
|
const Withdrawals = () => {
|
||||||
|
const { keypair } = useVegaWallet();
|
||||||
|
|
||||||
|
if (!keypair) {
|
||||||
|
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Web3Container
|
||||||
|
render={() => (
|
||||||
|
<div className="h-full grid grid grid-rows-[min-content,1fr]">
|
||||||
|
<header className="flex justify-between p-24">
|
||||||
|
<h1 className="text-h3">{t('Withdrawals')}</h1>
|
||||||
|
<AnchorButton href="/portfolio/withdraw">
|
||||||
|
{t('Start withdrawal')}
|
||||||
|
</AnchorButton>
|
||||||
|
</header>
|
||||||
|
<WithdrawalsPageContainer />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Withdrawals;
|
@ -0,0 +1,23 @@
|
|||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useWithdrawals, WithdrawalsTable } from '@vegaprotocol/withdraws';
|
||||||
|
|
||||||
|
export const WithdrawalsPageContainer = () => {
|
||||||
|
const { data, loading, error } = useWithdrawals();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer
|
||||||
|
data={data}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
render={(data) => {
|
||||||
|
const withdrawals = orderBy(
|
||||||
|
data.party?.withdrawals || [],
|
||||||
|
(w) => new Date(w.createdTimestamp).getTime(),
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
return <WithdrawalsTable withdrawals={withdrawals} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -32,7 +32,7 @@ export const DealTicketManager = ({ market }: DealTicketManagerProps) => {
|
|||||||
return Intent.Danger;
|
return Intent.Danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === VegaTxStatus.Rejected) {
|
if (status === VegaTxStatus.Error) {
|
||||||
return Intent.Danger;
|
return Intent.Danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export const DealTicketManager = ({ market }: DealTicketManagerProps) => {
|
|||||||
market={market}
|
market={market}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
transactionStatus={
|
transactionStatus={
|
||||||
transaction.status === VegaTxStatus.AwaitingConfirmation ||
|
transaction.status === VegaTxStatus.Requested ||
|
||||||
transaction.status === VegaTxStatus.Pending
|
transaction.status === VegaTxStatus.Pending
|
||||||
? 'pending'
|
? 'pending'
|
||||||
: 'default'
|
: 'default'
|
||||||
|
@ -2,11 +2,11 @@ import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent';
|
import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent';
|
||||||
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
import type { TransactionState } from '@vegaprotocol/wallet';
|
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
interface OrderDialogProps {
|
interface OrderDialogProps {
|
||||||
transaction: TransactionState;
|
transaction: VegaTxState;
|
||||||
finalizedOrder: OrderEvent_busEvents_event_Order | null;
|
finalizedOrder: OrderEvent_busEvents_event_Order | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const OrderDialog = ({
|
|||||||
// TODO: When wallets support confirming transactions return UI for 'awaiting confirmation' step
|
// TODO: When wallets support confirming transactions return UI for 'awaiting confirmation' step
|
||||||
|
|
||||||
// Rejected by wallet
|
// Rejected by wallet
|
||||||
if (transaction.status === VegaTxStatus.Rejected) {
|
if (transaction.status === VegaTxStatus.Error) {
|
||||||
return (
|
return (
|
||||||
<OrderDialogWrapper
|
<OrderDialogWrapper
|
||||||
title="Order rejected by wallet"
|
title="Order rejected by wallet"
|
||||||
@ -39,9 +39,9 @@ export const OrderDialog = ({
|
|||||||
title="Awaiting network confirmation"
|
title="Awaiting network confirmation"
|
||||||
icon={<Loader size="small" />}
|
icon={<Loader size="small" />}
|
||||||
>
|
>
|
||||||
{transaction.hash && (
|
{transaction.txHash && (
|
||||||
<p data-testid="tx-hash" className="break-all">
|
<p data-testid="tx-hash" className="break-all">
|
||||||
{t(`Tx hash: ${transaction.hash}`)}
|
{t(`Tx hash: ${transaction.txHash}`)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</OrderDialogWrapper>
|
</OrderDialogWrapper>
|
||||||
|
@ -41,7 +41,7 @@ test('Has the correct default state', () => {
|
|||||||
expect(typeof result.current.submit).toEqual('function');
|
expect(typeof result.current.submit).toEqual('function');
|
||||||
expect(typeof result.current.reset).toEqual('function');
|
expect(typeof result.current.reset).toEqual('function');
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||||
expect(result.current.transaction.hash).toEqual(null);
|
expect(result.current.transaction.txHash).toEqual(null);
|
||||||
expect(result.current.transaction.error).toEqual(null);
|
expect(result.current.transaction.error).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { gql, useSubscription } from '@apollo/client';
|
import { gql, useSubscription } from '@apollo/client';
|
||||||
import { ethers } from 'ethers';
|
|
||||||
import { SHA3 } from 'sha3';
|
|
||||||
import type { Order } from './use-order-state';
|
import type { Order } from './use-order-state';
|
||||||
import { OrderType, useVegaWallet } from '@vegaprotocol/wallet';
|
import { OrderType, useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
|
||||||
import { useVegaTransaction } from '@vegaprotocol/wallet';
|
import { useVegaTransaction } from '@vegaprotocol/wallet';
|
||||||
import type {
|
import type {
|
||||||
OrderEvent,
|
OrderEvent,
|
||||||
@ -132,23 +130,3 @@ export const useOrderSubmit = (market: UseOrderSubmitMarket) => {
|
|||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This function creates an ID in the same way that core does on the backend. This way we
|
|
||||||
* Can match up the newly created order with incoming orders via a subscription
|
|
||||||
*/
|
|
||||||
export const determineId = (sig: string) => {
|
|
||||||
// Prepend 0x
|
|
||||||
if (sig.slice(0, 2) !== '0x') {
|
|
||||||
sig = '0x' + sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the ID
|
|
||||||
const hash = new SHA3(256);
|
|
||||||
const bytes = ethers.utils.arrayify(sig);
|
|
||||||
hash.update(Buffer.from(bytes));
|
|
||||||
const id = ethers.utils.hexlify(hash.digest());
|
|
||||||
|
|
||||||
// Remove 0x as core doesn't keep them in the API
|
|
||||||
return id.substring(2);
|
|
||||||
};
|
|
||||||
|
@ -118,7 +118,8 @@ test('Form validation', async () => {
|
|||||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
target: { value: amountLessThanMinViable },
|
target: { value: amountLessThanMinViable },
|
||||||
});
|
});
|
||||||
expect(await screen.findByText('Amount is below permitted minimum'));
|
|
||||||
|
expect(await screen.findByText('Value is below minimum'));
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
<DepositForm
|
<DepositForm
|
||||||
@ -130,7 +131,7 @@ test('Form validation', async () => {
|
|||||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
target: { value: amountLessThanLimit },
|
target: { value: amountLessThanLimit },
|
||||||
});
|
});
|
||||||
expect(await screen.findByText('Amount is below permitted minimum'));
|
expect(await screen.findByText('Value is below minimum'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Approval', () => {
|
test('Approval', () => {
|
||||||
|
@ -6,13 +6,9 @@ import sortBy from 'lodash/sortBy';
|
|||||||
import { useSubmitApproval } from './use-submit-approval';
|
import { useSubmitApproval } from './use-submit-approval';
|
||||||
import { useGetDepositLimits } from './use-get-deposit-limits';
|
import { useGetDepositLimits } from './use-get-deposit-limits';
|
||||||
import { useGetAllowance } from './use-get-allowance';
|
import { useGetAllowance } from './use-get-allowance';
|
||||||
import { TransactionDialog } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useSubmitFaucet } from './use-submit-faucet';
|
import { useSubmitFaucet } from './use-submit-faucet';
|
||||||
import {
|
import { EthTxStatus, TransactionDialog } from '@vegaprotocol/web3';
|
||||||
useTokenContract,
|
import { useTokenContract, useBridgeContract } from '@vegaprotocol/web3';
|
||||||
useBridgeContract,
|
|
||||||
TxState,
|
|
||||||
} from '@vegaprotocol/react-helpers';
|
|
||||||
|
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
__typename: 'Asset';
|
__typename: 'Asset';
|
||||||
@ -76,10 +72,13 @@ export const DepositManager = ({
|
|||||||
|
|
||||||
// Update balance after confirmation event has been received
|
// Update balance after confirmation event has been received
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (faucet.status === TxState.Complete || confirmationEvent !== null) {
|
if (
|
||||||
|
faucet.transaction.status === EthTxStatus.Complete ||
|
||||||
|
confirmationEvent !== null
|
||||||
|
) {
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
}, [confirmationEvent, refetch, faucet.status]);
|
}, [confirmationEvent, refetch, faucet.transaction.status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -94,8 +93,8 @@ export const DepositManager = ({
|
|||||||
limits={limits}
|
limits={limits}
|
||||||
allowance={allowance}
|
allowance={allowance}
|
||||||
/>
|
/>
|
||||||
<TransactionDialog {...approve} name="approve" />
|
<TransactionDialog {...approve.transaction} name="approve" />
|
||||||
<TransactionDialog {...faucet} name="faucet" />
|
<TransactionDialog {...faucet.transaction} name="faucet" />
|
||||||
<TransactionDialog
|
<TransactionDialog
|
||||||
{...deposit}
|
{...deposit}
|
||||||
name="deposit"
|
name="deposit"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
import { useEthereumReadContract } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
export const useGetAllowance = (
|
export const useGetAllowance = (
|
||||||
contract: ERC20Token | null,
|
contract: ERC20Token | null,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
import { useEthereumReadContract } from '@vegaprotocol/web3';
|
||||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
import { useWeb3React } from '@web3-react/core';
|
import { useWeb3React } from '@web3-react/core';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
@ -2,7 +2,7 @@ import type BigNumber from 'bignumber.js';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
import type { Asset } from './deposit-manager';
|
import type { Asset } from './deposit-manager';
|
||||||
import { useEthereumReadContract } from '@vegaprotocol/react-helpers';
|
import { useEthereumReadContract } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
interface Limits {
|
interface Limits {
|
||||||
min: BigNumber;
|
min: BigNumber;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
import { useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
import { useEthereumTransaction } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
export const useSubmitApproval = (
|
export const useSubmitApproval = (
|
||||||
contract: ERC20Token | null,
|
contract: ERC20Token | null,
|
||||||
|
@ -6,7 +6,8 @@ import type {
|
|||||||
} from './__generated__/DepositEvent';
|
} from './__generated__/DepositEvent';
|
||||||
import { DepositStatus } from '@vegaprotocol/types';
|
import { DepositStatus } from '@vegaprotocol/types';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { remove0x, useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
import { remove0x } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useEthereumTransaction } from '@vegaprotocol/web3';
|
||||||
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
import type { VegaErc20Bridge } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
|
|
||||||
const DEPOSIT_EVENT_SUB = gql`
|
const DEPOSIT_EVENT_SUB = gql`
|
||||||
@ -33,7 +34,7 @@ export const useSubmitDeposit = (
|
|||||||
// NOTE: it may be different from the users connected key
|
// NOTE: it may be different from the users connected key
|
||||||
const [partyId, setPartyId] = useState<string | null>(null);
|
const [partyId, setPartyId] = useState<string | null>(null);
|
||||||
|
|
||||||
const transaction = useEthereumTransaction<{
|
const { transaction, perform } = useEthereumTransaction<{
|
||||||
assetSource: string;
|
assetSource: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
vegaPublicKey: string;
|
vegaPublicKey: string;
|
||||||
@ -87,6 +88,7 @@ export const useSubmitDeposit = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...transaction,
|
...transaction,
|
||||||
|
perform,
|
||||||
confirmationEvent,
|
confirmationEvent,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
import type { ERC20Token } from '@vegaprotocol/smart-contracts-sdk';
|
||||||
import { useEthereumTransaction } from '@vegaprotocol/react-helpers';
|
import { useEthereumTransaction } from '@vegaprotocol/web3';
|
||||||
|
|
||||||
export const useSubmitFaucet = (contract: ERC20Token | null) => {
|
export const useSubmitFaucet = (contract: ERC20Token | null) => {
|
||||||
const transaction = useEthereumTransaction(() => {
|
const transaction = useEthereumTransaction(() => {
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
export * from './use-apply-grid-transaction';
|
export * from './use-apply-grid-transaction';
|
||||||
export * from './use-ethereum-transaction';
|
|
||||||
export * from './use-ethereum-read-contract';
|
|
||||||
export * from './use-data-provider';
|
export * from './use-data-provider';
|
||||||
export * from './use-theme-switcher';
|
export * from './use-theme-switcher';
|
||||||
export * from './use-bridge-contract';
|
|
||||||
export * from './use-token-contract';
|
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import type { ethers } from 'ethers';
|
|
||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { EthereumError, isEthereumError } from '../lib/ethereum-error';
|
|
||||||
|
|
||||||
export enum TxState {
|
|
||||||
Default = 'Default',
|
|
||||||
Requested = 'Requested',
|
|
||||||
Pending = 'Pending',
|
|
||||||
Complete = 'Complete',
|
|
||||||
Error = 'Error',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TxError = Error | EthereumError;
|
|
||||||
|
|
||||||
export const useEthereumTransaction = <TArgs = void>(
|
|
||||||
performTransaction: (
|
|
||||||
args: TArgs
|
|
||||||
) => Promise<ethers.ContractTransaction> | null,
|
|
||||||
requiredConfirmations = 1
|
|
||||||
) => {
|
|
||||||
const [confirmations, setConfirmations] = useState(0);
|
|
||||||
const [status, setStatus] = useState(TxState.Default);
|
|
||||||
const [txHash, setTxHash] = useState<string | null>(null);
|
|
||||||
const [error, setError] = useState<TxError | null>(null);
|
|
||||||
|
|
||||||
const perform = useCallback(
|
|
||||||
async (args: TArgs) => {
|
|
||||||
setConfirmations(0);
|
|
||||||
setStatus(TxState.Requested);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = performTransaction(args);
|
|
||||||
|
|
||||||
if (res === null) {
|
|
||||||
setStatus(TxState.Default);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = await res;
|
|
||||||
|
|
||||||
let receipt: ethers.ContractReceipt | null = null;
|
|
||||||
|
|
||||||
setTxHash(tx.hash);
|
|
||||||
setStatus(TxState.Pending);
|
|
||||||
|
|
||||||
for (let i = 1; i <= requiredConfirmations; i++) {
|
|
||||||
receipt = await tx.wait(i);
|
|
||||||
setConfirmations(receipt.confirmations);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!receipt) {
|
|
||||||
throw new Error('No receipt after confirmations are met');
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(TxState.Complete);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
setError(err);
|
|
||||||
} else if (isEthereumError(err)) {
|
|
||||||
setError(new EthereumError(err.message, err.code));
|
|
||||||
} else {
|
|
||||||
setError(new Error('Something went wrong'));
|
|
||||||
}
|
|
||||||
setStatus(TxState.Error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[performTransaction, requiredConfirmations]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { perform, status, error, confirmations, txHash };
|
|
||||||
};
|
|
@ -1,6 +1,5 @@
|
|||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './lib/context';
|
export * from './lib/context';
|
||||||
export * from './lib/ethereum-error';
|
|
||||||
export * from './lib/format';
|
export * from './lib/format';
|
||||||
export * from './lib/generic-data-provider';
|
export * from './lib/generic-data-provider';
|
||||||
export * from './lib/grid';
|
export * from './lib/grid';
|
||||||
@ -8,3 +7,4 @@ export * from './lib/i18n';
|
|||||||
export * from './lib/remove-0x';
|
export * from './lib/remove-0x';
|
||||||
export * from './lib/storage';
|
export * from './lib/storage';
|
||||||
export * from './lib/validate';
|
export * from './lib/validate';
|
||||||
|
export * from './lib/determine-id';
|
||||||
|
23
libs/react-helpers/src/lib/determine-id.ts
Normal file
23
libs/react-helpers/src/lib/determine-id.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { SHA3 } from 'sha3';
|
||||||
|
import { remove0x } from './remove-0x';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function creates an ID in the same way that core does on the backend. This way we
|
||||||
|
* Can match up the newly created order with incoming orders via a subscription
|
||||||
|
*/
|
||||||
|
export const determineId = (sig: string) => {
|
||||||
|
// Prepend 0x
|
||||||
|
if (sig.slice(0, 2) !== '0x') {
|
||||||
|
sig = '0x' + sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the ID
|
||||||
|
const hash = new SHA3(256);
|
||||||
|
const bytes = ethers.utils.arrayify(sig);
|
||||||
|
hash.update(Buffer.from(bytes));
|
||||||
|
const id = ethers.utils.hexlify(hash.digest());
|
||||||
|
|
||||||
|
// Remove 0x as core doesn't keep them in the API
|
||||||
|
return remove0x(id);
|
||||||
|
};
|
10
libs/react-helpers/src/lib/determine-ids.test.ts
Normal file
10
libs/react-helpers/src/lib/determine-ids.test.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { determineId } from './determine-id';
|
||||||
|
|
||||||
|
test('It produces a known result for an ID', () => {
|
||||||
|
const res = determineId(
|
||||||
|
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909'
|
||||||
|
);
|
||||||
|
expect(res).toStrictEqual(
|
||||||
|
'2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50'
|
||||||
|
);
|
||||||
|
});
|
@ -1 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* A stand in for real i18n - this function should
|
||||||
|
* be used wherever text is placed in the UI so that
|
||||||
|
* in future they can easily be extracted
|
||||||
|
*
|
||||||
|
* @param str A
|
||||||
|
* @returns str A
|
||||||
|
*/
|
||||||
export const t = (str: string) => str;
|
export const t = (str: string) => str;
|
||||||
|
38
libs/react-helpers/src/lib/remove-0x.test.ts
Normal file
38
libs/react-helpers/src/lib/remove-0x.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { remove0x } from './remove-0x';
|
||||||
|
|
||||||
|
it('Removes 0x from the start of a transaction hash', () => {
|
||||||
|
const randomEthereumTx =
|
||||||
|
'0xfc1fe28cb9cb255c93beaa6ceb3313d41ba17475489e0874fc46d64376d60088';
|
||||||
|
const res = remove0x(randomEthereumTx);
|
||||||
|
|
||||||
|
expect(res).toStrictEqual(
|
||||||
|
'fc1fe28cb9cb255c93beaa6ceb3313d41ba17475489e0874fc46d64376d60088'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not check if the string is valid hex', () => {
|
||||||
|
const invalidData = '**Not-Hex-At-All!!**';
|
||||||
|
const res = remove0x(`0x${invalidData}`);
|
||||||
|
|
||||||
|
expect(res).toStrictEqual(invalidData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not remove 0x in the middle of the string', () => {
|
||||||
|
const unprefixedString = 'test-0x-test';
|
||||||
|
const res = remove0x(unprefixedString);
|
||||||
|
|
||||||
|
expect(res).toStrictEqual(unprefixedString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not remove all 0xs, just one at the start', () => {
|
||||||
|
const weirdString = '0x0x0x';
|
||||||
|
const res = remove0x(weirdString);
|
||||||
|
|
||||||
|
expect(res).toStrictEqual('0x0x');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles non strings by returning them untouched', () => {
|
||||||
|
const res = remove0x(NaN as unknown as string);
|
||||||
|
|
||||||
|
expect(res).toStrictEqual(NaN);
|
||||||
|
});
|
@ -1,3 +1,16 @@
|
|||||||
|
import isString from 'lodash/isString';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a hex prefix from the start of a string
|
||||||
|
*
|
||||||
|
* @param str Hex string
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
export const remove0x = (str: string) => {
|
export const remove0x = (str: string) => {
|
||||||
|
// Should be prevented by typescript, but just in case...
|
||||||
|
if (!isString(str)) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
return str.replace(/^0x/, '');
|
return str.replace(/^0x/, '');
|
||||||
};
|
};
|
||||||
|
@ -25,14 +25,14 @@ export const vegaPublicKey = (value: string) => {
|
|||||||
|
|
||||||
export const minSafe = (min: BigNumber) => (value: string) => {
|
export const minSafe = (min: BigNumber) => (value: string) => {
|
||||||
if (new BigNumber(value).isLessThan(min)) {
|
if (new BigNumber(value).isLessThan(min)) {
|
||||||
return t('Amount is below permitted minimum');
|
return t('Value is below minimum');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxSafe = (max: BigNumber) => (value: string) => {
|
export const maxSafe = (max: BigNumber) => (value: string) => {
|
||||||
if (new BigNumber(value).isGreaterThan(max)) {
|
if (new BigNumber(value).isGreaterThan(max)) {
|
||||||
return t('Amount is above permitted maximum');
|
return t('Value is above maximum');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
20
libs/types/src/__generated__/globalTypes.ts
generated
20
libs/types/src/__generated__/globalTypes.ts
generated
@ -79,7 +79,7 @@ export enum DepositStatus {
|
|||||||
/**
|
/**
|
||||||
* The interval for trade candles when subscribing via VEGA graphql, default is I15M
|
* The interval for trade candles when subscribing via VEGA graphql, default is I15M
|
||||||
*/
|
*/
|
||||||
export enum Interval {
|
export enum Interval {
|
||||||
I15M = "I15M",
|
I15M = "I15M",
|
||||||
I1D = "I1D",
|
I1D = "I1D",
|
||||||
I1H = "I1H",
|
I1H = "I1H",
|
||||||
@ -265,11 +265,29 @@ export enum Side {
|
|||||||
Sell = "Sell",
|
Sell = "Sell",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the stake linking
|
||||||
|
*/
|
||||||
|
export enum StakeLinkingStatus {
|
||||||
|
Accepted = "Accepted",
|
||||||
|
Pending = "Pending",
|
||||||
|
Rejected = "Rejected",
|
||||||
|
}
|
||||||
|
|
||||||
export enum VoteValue {
|
export enum VoteValue {
|
||||||
No = "No",
|
No = "No",
|
||||||
Yes = "Yes",
|
Yes = "Yes",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of a withdrawal
|
||||||
|
*/
|
||||||
|
export enum WithdrawalStatus {
|
||||||
|
Finalized = "Finalized",
|
||||||
|
Open = "Open",
|
||||||
|
Rejected = "Rejected",
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================
|
//==============================================================
|
||||||
// END Enums and Input Objects
|
// END Enums and Input Objects
|
||||||
//==============================================================
|
//==============================================================
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Splash } from '../splash';
|
import { Splash } from '../splash';
|
||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
interface AsyncRendererProps<T> {
|
interface AsyncRendererProps<T> {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -17,15 +18,15 @@ export function AsyncRenderer<T = object>({
|
|||||||
render,
|
render,
|
||||||
}: AsyncRendererProps<T>) {
|
}: AsyncRendererProps<T>) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Splash>Loading...</Splash>;
|
return <Splash>{t('Loading...')}</Splash>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <Splash>No data</Splash>;
|
return <Splash>{t('No data')}</Splash>;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{render ? render(data) : children}</>;
|
return <>{render ? render(data) : children}</>;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { AnchorHTMLAttributes } from 'react';
|
import type { AnchorHTMLAttributes } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const ETHERSCAN_URL = process.env['NX_ETHERSCAN_URL'] as string;
|
const ETHERSCAN_URL = process.env['NX_ETHERSCAN_URL'] as string;
|
||||||
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
|
||||||
import { TransactionDialog } from './transaction-dialog';
|
|
||||||
import { TxState } from '@vegaprotocol/react-helpers';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: TransactionDialog,
|
|
||||||
title: 'TransactionDialog',
|
|
||||||
parameters: {
|
|
||||||
themes: false,
|
|
||||||
},
|
|
||||||
} as ComponentMeta<typeof TransactionDialog>;
|
|
||||||
|
|
||||||
const Template: ComponentStory<typeof TransactionDialog> = (args) => (
|
|
||||||
<TransactionDialog {...args} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Requested = Template.bind({});
|
|
||||||
Requested.args = {
|
|
||||||
name: 'Some tx',
|
|
||||||
status: TxState.Requested,
|
|
||||||
error: null,
|
|
||||||
confirmations: 0,
|
|
||||||
txHash: null,
|
|
||||||
requiredConfirmations: 1,
|
|
||||||
confirmed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Pending = Template.bind({});
|
|
||||||
Pending.args = {
|
|
||||||
name: 'Some tx',
|
|
||||||
status: TxState.Pending,
|
|
||||||
error: null,
|
|
||||||
confirmations: 1,
|
|
||||||
txHash: '0x123123',
|
|
||||||
requiredConfirmations: 3,
|
|
||||||
confirmed: false,
|
|
||||||
};
|
|
||||||
export const Error = Template.bind({});
|
|
||||||
Error.args = {
|
|
||||||
name: 'Some tx',
|
|
||||||
status: TxState.Error,
|
|
||||||
error: {
|
|
||||||
name: 'Error',
|
|
||||||
message:
|
|
||||||
'Some very long error message with text that should wrap, here is some error data: {"chain":3,"data":"0x08c379000000000000000000000000000000000000000000000000000000000"}',
|
|
||||||
},
|
|
||||||
confirmations: 0,
|
|
||||||
txHash: null,
|
|
||||||
requiredConfirmations: 1,
|
|
||||||
confirmed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Complete = Template.bind({});
|
|
||||||
Complete.args = {
|
|
||||||
name: 'Some tx',
|
|
||||||
status: TxState.Complete,
|
|
||||||
error: null,
|
|
||||||
confirmations: 3,
|
|
||||||
txHash: '0x123123',
|
|
||||||
requiredConfirmations: 3,
|
|
||||||
confirmed: true,
|
|
||||||
};
|
|
@ -21,7 +21,6 @@ export { VegaLogo } from './components/vega-logo';
|
|||||||
export { Tooltip } from './components/tooltip';
|
export { Tooltip } from './components/tooltip';
|
||||||
export { Indicator } from './components/indicator';
|
export { Indicator } from './components/indicator';
|
||||||
export { Card } from './components/card';
|
export { Card } from './components/card';
|
||||||
export { TransactionDialog } from './components/transaction-dialog';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export * from './utils/intent';
|
export * from './utils/intent';
|
||||||
|
@ -2,6 +2,7 @@ import type {
|
|||||||
VegaKey,
|
VegaKey,
|
||||||
TransactionResponse,
|
TransactionResponse,
|
||||||
OrderSubmissionBody,
|
OrderSubmissionBody,
|
||||||
|
WithdrawSubmissionBody,
|
||||||
} from '@vegaprotocol/vegawallet-service-api-client';
|
} from '@vegaprotocol/vegawallet-service-api-client';
|
||||||
export { RestConnector } from './rest-connector';
|
export { RestConnector } from './rest-connector';
|
||||||
|
|
||||||
@ -25,6 +26,6 @@ export interface VegaConnector {
|
|||||||
|
|
||||||
/** Send a TX to the network. Only support order submission for now */
|
/** Send a TX to the network. Only support order submission for now */
|
||||||
sendTx: (
|
sendTx: (
|
||||||
body: OrderSubmissionBody
|
body: OrderSubmissionBody | WithdrawSubmissionBody
|
||||||
) => Promise<TransactionResponse | ErrorResponse>;
|
) => Promise<TransactionResponse | ErrorResponse>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
Configuration,
|
Configuration,
|
||||||
OrderSubmissionBody,
|
OrderSubmissionBody,
|
||||||
|
WithdrawSubmissionBody,
|
||||||
} from '@vegaprotocol/vegawallet-service-api-client';
|
} from '@vegaprotocol/vegawallet-service-api-client';
|
||||||
import {
|
import {
|
||||||
createConfiguration,
|
createConfiguration,
|
||||||
@ -87,7 +88,7 @@ export class RestConnector implements VegaConnector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendTx(body: OrderSubmissionBody) {
|
async sendTx(body: OrderSubmissionBody | WithdrawSubmissionBody) {
|
||||||
try {
|
try {
|
||||||
return await this.service.commandSyncPost(body);
|
return await this.service.commandSyncPost(body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -4,7 +4,7 @@ import type {
|
|||||||
} from '@vegaprotocol/vegawallet-service-api-client';
|
} from '@vegaprotocol/vegawallet-service-api-client';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import type { VegaConnector } from './connectors';
|
import type { VegaConnector } from './connectors';
|
||||||
import type { Transaction } from './types';
|
import type { TransactionSubmission } from './types';
|
||||||
|
|
||||||
export type SendTxError =
|
export type SendTxError =
|
||||||
| {
|
| {
|
||||||
@ -39,7 +39,7 @@ export interface VegaWalletContextShape {
|
|||||||
|
|
||||||
/** Send a transaction to the network, only order submissions for now */
|
/** Send a transaction to the network, only order submissions for now */
|
||||||
sendTx: (
|
sendTx: (
|
||||||
tx: Transaction
|
tx: TransactionSubmission
|
||||||
) => Promise<TransactionResponse | SendTxError> | null;
|
) => Promise<TransactionResponse | SendTxError> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@ import type { VegaKeyExtended, VegaWalletContextShape } from '.';
|
|||||||
import type { VegaConnector } from './connectors';
|
import type { VegaConnector } from './connectors';
|
||||||
import { VegaWalletContext } from './context';
|
import { VegaWalletContext } from './context';
|
||||||
import { WALLET_KEY } from './storage-keys';
|
import { WALLET_KEY } from './storage-keys';
|
||||||
import type { OrderSubmissionBody } from '@vegaprotocol/vegawallet-service-api-client';
|
import type {
|
||||||
|
OrderSubmissionBody,
|
||||||
|
WithdrawSubmissionBody,
|
||||||
|
} from '@vegaprotocol/vegawallet-service-api-client';
|
||||||
|
|
||||||
interface VegaWalletProviderProps {
|
interface VegaWalletProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -69,13 +72,16 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sendTx = useCallback((body: OrderSubmissionBody) => {
|
const sendTx = useCallback(
|
||||||
if (!connector.current) {
|
(body: OrderSubmissionBody | WithdrawSubmissionBody) => {
|
||||||
return null;
|
if (!connector.current) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return connector.current.sendTx(body);
|
return connector.current.sendTx(body);
|
||||||
}, []);
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Current selected keypair derived from publicKey state
|
// Current selected keypair derived from publicKey state
|
||||||
const keypair = useMemo(() => {
|
const keypair = useMemo(() => {
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import type {
|
||||||
|
OrderSubmissionBody,
|
||||||
|
WithdrawSubmissionBody,
|
||||||
|
} from '@vegaprotocol/vegawallet-service-api-client';
|
||||||
|
|
||||||
export enum OrderType {
|
export enum OrderType {
|
||||||
Market = 'TYPE_MARKET',
|
Market = 'TYPE_MARKET',
|
||||||
Limit = 'TYPE_LIMIT',
|
Limit = 'TYPE_LIMIT',
|
||||||
@ -17,20 +22,7 @@ export enum OrderTimeInForce {
|
|||||||
GFA = 'TIME_IN_FORCE_GFA',
|
GFA = 'TIME_IN_FORCE_GFA',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderSubmission {
|
|
||||||
pubKey: string;
|
|
||||||
propagate: boolean;
|
|
||||||
orderSubmission: {
|
|
||||||
marketId: string;
|
|
||||||
size: string;
|
|
||||||
type: OrderType;
|
|
||||||
side: OrderSide;
|
|
||||||
timeInForce: OrderTimeInForce;
|
|
||||||
price?: string;
|
|
||||||
expiresAt?: string;
|
|
||||||
reference?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will make Transaction a union type as other transactions are added
|
// Will make Transaction a union type as other transactions are added
|
||||||
export type Transaction = OrderSubmission;
|
export type TransactionSubmission =
|
||||||
|
| OrderSubmissionBody
|
||||||
|
| WithdrawSubmissionBody;
|
||||||
|
@ -27,7 +27,7 @@ function setup(context?: Partial<VegaWalletContextShape>) {
|
|||||||
test('Has the correct default state', () => {
|
test('Has the correct default state', () => {
|
||||||
const { result } = setup();
|
const { result } = setup();
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||||
expect(result.current.transaction.hash).toEqual(null);
|
expect(result.current.transaction.txHash).toEqual(null);
|
||||||
expect(result.current.transaction.signature).toEqual(null);
|
expect(result.current.transaction.signature).toEqual(null);
|
||||||
expect(result.current.transaction.error).toEqual(null);
|
expect(result.current.transaction.error).toEqual(null);
|
||||||
expect(typeof result.current.reset).toEqual('function');
|
expect(typeof result.current.reset).toEqual('function');
|
||||||
@ -52,7 +52,7 @@ test('Handles a single error', async () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.send({} as OrderSubmission);
|
result.current.send({} as OrderSubmission);
|
||||||
});
|
});
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Rejected);
|
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
|
||||||
expect(result.current.transaction.error).toEqual({ error: errorMessage });
|
expect(result.current.transaction.error).toEqual({ error: errorMessage });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ test('Handles multiple errors', async () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.send({} as OrderSubmission);
|
result.current.send({} as OrderSubmission);
|
||||||
});
|
});
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Rejected);
|
expect(result.current.transaction.status).toEqual(VegaTxStatus.Error);
|
||||||
expect(result.current.transaction.error).toEqual(errorObj);
|
expect(result.current.transaction.error).toEqual(errorObj);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ test('Returns the signature if successful', async () => {
|
|||||||
result.current.send({} as OrderSubmission);
|
result.current.send({} as OrderSubmission);
|
||||||
});
|
});
|
||||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Pending);
|
expect(result.current.transaction.status).toEqual(VegaTxStatus.Pending);
|
||||||
expect(result.current.transaction.hash).toEqual(successObj.txHash);
|
expect(result.current.transaction.txHash).toEqual(successObj.txHash);
|
||||||
expect(result.current.transaction.signature).toEqual(
|
expect(result.current.transaction.signature).toEqual(
|
||||||
successObj.tx.signature.value
|
successObj.tx.signature.value
|
||||||
);
|
);
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import type { Transaction } from './types';
|
import type { TransactionSubmission } from './types';
|
||||||
import { useVegaWallet } from './hooks';
|
import { useVegaWallet } from './hooks';
|
||||||
import type { SendTxError } from './context';
|
import type { SendTxError } from './context';
|
||||||
|
|
||||||
export enum VegaTxStatus {
|
export enum VegaTxStatus {
|
||||||
Default = 'Default',
|
Default = 'Default',
|
||||||
AwaitingConfirmation = 'AwaitingConfirmation',
|
Requested = 'Requested',
|
||||||
Rejected = 'Rejected',
|
|
||||||
Pending = 'Pending',
|
Pending = 'Pending',
|
||||||
|
Error = 'Error',
|
||||||
|
// Note no complete state as we have to use api calls/subs to check if
|
||||||
|
// our transaction was completed
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionState {
|
export interface VegaTxState {
|
||||||
status: VegaTxStatus;
|
status: VegaTxStatus;
|
||||||
error: object | null;
|
error: object | null;
|
||||||
hash: string | null;
|
txHash: string | null;
|
||||||
signature: string | null;
|
signature: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
status: VegaTxStatus.Default,
|
||||||
|
error: null,
|
||||||
|
txHash: null,
|
||||||
|
signature: null,
|
||||||
|
};
|
||||||
|
|
||||||
export const useVegaTransaction = () => {
|
export const useVegaTransaction = () => {
|
||||||
const { sendTx } = useVegaWallet();
|
const { sendTx } = useVegaWallet();
|
||||||
const [transaction, _setTransaction] = useState<TransactionState>({
|
const [transaction, _setTransaction] = useState<VegaTxState>(initialState);
|
||||||
status: VegaTxStatus.Default,
|
|
||||||
error: null,
|
|
||||||
hash: null,
|
|
||||||
signature: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setTransaction = useCallback((update: Partial<TransactionState>) => {
|
const setTransaction = useCallback((update: Partial<VegaTxState>) => {
|
||||||
_setTransaction((curr) => ({
|
_setTransaction((curr) => ({
|
||||||
...curr,
|
...curr,
|
||||||
...update,
|
...update,
|
||||||
@ -35,18 +39,18 @@ export const useVegaTransaction = () => {
|
|||||||
|
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
(error: SendTxError) => {
|
(error: SendTxError) => {
|
||||||
setTransaction({ error, status: VegaTxStatus.Rejected });
|
setTransaction({ error, status: VegaTxStatus.Error });
|
||||||
},
|
},
|
||||||
[setTransaction]
|
[setTransaction]
|
||||||
);
|
);
|
||||||
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
async (tx: Transaction) => {
|
async (tx: TransactionSubmission) => {
|
||||||
setTransaction({
|
setTransaction({
|
||||||
error: null,
|
error: null,
|
||||||
hash: null,
|
txHash: null,
|
||||||
signature: null,
|
signature: null,
|
||||||
status: VegaTxStatus.AwaitingConfirmation,
|
status: VegaTxStatus.Requested,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await sendTx(tx);
|
const res = await sendTx(tx);
|
||||||
@ -65,7 +69,7 @@ export const useVegaTransaction = () => {
|
|||||||
} else if (res.tx?.signature?.value && res.txHash) {
|
} else if (res.tx?.signature?.value && res.txHash) {
|
||||||
setTransaction({
|
setTransaction({
|
||||||
status: VegaTxStatus.Pending,
|
status: VegaTxStatus.Pending,
|
||||||
hash: res.txHash,
|
txHash: res.txHash,
|
||||||
signature: res.tx.signature.value,
|
signature: res.tx.signature.value,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -79,12 +83,7 @@ export const useVegaTransaction = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setTransaction({
|
setTransaction(initialState);
|
||||||
error: null,
|
|
||||||
hash: null,
|
|
||||||
signature: null,
|
|
||||||
status: VegaTxStatus.Default,
|
|
||||||
});
|
|
||||||
}, [setTransaction]);
|
}, [setTransaction]);
|
||||||
|
|
||||||
return { send, transaction, reset };
|
return { send, transaction, reset };
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||||
"ignorePatterns": ["!**/*"],
|
"ignorePatterns": ["!**/*", "__generated__"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
4
libs/web3/package.json
Normal file
4
libs/web3/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@vegaprotocol/web3",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
@ -4,6 +4,26 @@
|
|||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"targets": {
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nrwl/web:rollup",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/web3",
|
||||||
|
"tsConfig": "libs/web3/tsconfig.lib.json",
|
||||||
|
"project": "libs/web3/package.json",
|
||||||
|
"entryFile": "libs/web3/src/index.ts",
|
||||||
|
"external": ["react/jsx-runtime"],
|
||||||
|
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||||
|
"compiler": "babel",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "libs/web3/README.md",
|
||||||
|
"input": ".",
|
||||||
|
"output": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nrwl/linter:eslint",
|
"executor": "@nrwl/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
|
@ -1,2 +1,8 @@
|
|||||||
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/ethereum-error';
|
||||||
|
export * from './lib/use-bridge-contract';
|
||||||
|
export * from './lib/use-token-contract';
|
||||||
|
export * from './lib/use-ethereum-read-contract';
|
||||||
|
export * from './lib/use-ethereum-transaction';
|
||||||
|
export * from './lib/transaction-dialog';
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { EtherscanLink } from '../etherscan-link';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { t, TxState } from '@vegaprotocol/react-helpers';
|
import { EtherscanLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
|
|
||||||
const ACTIVE_CLASSES = 'text-black dark:text-white';
|
const ACTIVE_CLASSES = 'text-black dark:text-white';
|
||||||
|
|
||||||
export const ConfirmRow = ({ status }: { status: TxState }) => {
|
export const ConfirmRow = ({ status }: { status: EthTxStatus }) => {
|
||||||
if (status === TxState.Requested) {
|
if (status === EthTxStatus.Requested) {
|
||||||
return (
|
return (
|
||||||
<p className="text-black dark:text-white">
|
<p className="text-black dark:text-white">
|
||||||
{t('Confirm transaction in wallet')}
|
{t('Confirm transaction in wallet')}
|
||||||
@ -16,7 +17,7 @@ export const ConfirmRow = ({ status }: { status: TxState }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface TxRowProps {
|
interface TxRowProps {
|
||||||
status: TxState;
|
status: EthTxStatus;
|
||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
confirmations: number;
|
confirmations: number;
|
||||||
requiredConfirmations: number;
|
requiredConfirmations: number;
|
||||||
@ -30,7 +31,7 @@ export const TxRow = ({
|
|||||||
requiredConfirmations,
|
requiredConfirmations,
|
||||||
highlightComplete = true,
|
highlightComplete = true,
|
||||||
}: TxRowProps) => {
|
}: TxRowProps) => {
|
||||||
if (status === TxState.Pending) {
|
if (status === EthTxStatus.Pending) {
|
||||||
return (
|
return (
|
||||||
<p className={`flex justify-between ${ACTIVE_CLASSES}`}>
|
<p className={`flex justify-between ${ACTIVE_CLASSES}`}>
|
||||||
<span>
|
<span>
|
||||||
@ -47,7 +48,7 @@ export const TxRow = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === TxState.Complete) {
|
if (status === EthTxStatus.Complete) {
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={`flex justify-between ${
|
className={`flex justify-between ${
|
||||||
@ -68,7 +69,7 @@ export const TxRow = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ConfirmationEventRowProps {
|
interface ConfirmationEventRowProps {
|
||||||
status: TxState;
|
status: EthTxStatus;
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ export const ConfirmationEventRow = ({
|
|||||||
status,
|
status,
|
||||||
confirmed,
|
confirmed,
|
||||||
}: ConfirmationEventRowProps) => {
|
}: ConfirmationEventRowProps) => {
|
||||||
if (status !== TxState.Complete) {
|
if (status !== EthTxStatus.Complete) {
|
||||||
return <p>{t('Vega confirmation')}</p>;
|
return <p>{t('Vega confirmation')}</p>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { EthereumError, TxState } from '@vegaprotocol/react-helpers';
|
import { EthereumError } from '../ethereum-error';
|
||||||
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
import type { TransactionDialogProps } from './transaction-dialog';
|
import type { TransactionDialogProps } from './transaction-dialog';
|
||||||
import { TransactionDialog } from './transaction-dialog';
|
import { TransactionDialog } from './transaction-dialog';
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ let props: TransactionDialogProps;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
status: TxState.Default,
|
status: EthTxStatus.Default,
|
||||||
txHash: null,
|
txHash: null,
|
||||||
error: null,
|
error: null,
|
||||||
confirmations: 1,
|
confirmations: 1,
|
||||||
@ -31,14 +32,14 @@ test('Opens when tx starts and closes if the user rejects the tx', () => {
|
|||||||
// Dialog closed by default
|
// Dialog closed by default
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
|
||||||
rerender(generateJsx({ status: TxState.Pending }));
|
rerender(generateJsx({ status: EthTxStatus.Pending }));
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
|
|
||||||
// User rejecting the tx closes the dialog
|
// User rejecting the tx closes the dialog
|
||||||
rerender(
|
rerender(
|
||||||
generateJsx({
|
generateJsx({
|
||||||
status: TxState.Error,
|
status: EthTxStatus.Error,
|
||||||
error: new EthereumError('User rejected', 4001),
|
error: new EthereumError('User rejected', 4001),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -48,26 +49,26 @@ test('Opens when tx starts and closes if the user rejects the tx', () => {
|
|||||||
|
|
||||||
test('Doesn\t repoen if user dismissed the dialog', () => {
|
test('Doesn\t repoen if user dismissed the dialog', () => {
|
||||||
const { container, rerender } = render(
|
const { container, rerender } = render(
|
||||||
generateJsx({ status: TxState.Pending })
|
generateJsx({ status: EthTxStatus.Pending })
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('dialog-close'));
|
fireEvent.click(screen.getByTestId('dialog-close'));
|
||||||
|
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
|
||||||
rerender(generateJsx({ status: TxState.Complete }));
|
rerender(generateJsx({ status: EthTxStatus.Complete }));
|
||||||
|
|
||||||
// Should still be closed even though tx updated
|
// Should still be closed even though tx updated
|
||||||
expect(container).toBeEmptyDOMElement();
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Dialog states', () => {
|
test('Dialog states', () => {
|
||||||
const { rerender } = render(generateJsx({ status: TxState.Requested }));
|
const { rerender } = render(generateJsx({ status: EthTxStatus.Requested }));
|
||||||
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
expect(screen.getByText('Confirm transaction')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirm transaction in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirm transaction in wallet')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Await Ethereum transaction')).toBeInTheDocument();
|
expect(screen.getByText('Await Ethereum transaction')).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(generateJsx({ status: TxState.Pending, confirmations: 0 }));
|
rerender(generateJsx({ status: EthTxStatus.Pending, confirmations: 0 }));
|
||||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
@ -75,20 +76,22 @@ test('Dialog states', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('etherscan-link')).toBeInTheDocument();
|
expect(screen.getByTestId('etherscan-link')).toBeInTheDocument();
|
||||||
|
|
||||||
rerender(generateJsx({ status: TxState.Complete, confirmations: 1 }));
|
rerender(generateJsx({ status: EthTxStatus.Complete, confirmations: 1 }));
|
||||||
expect(screen.getByText(`${props.name} complete`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} complete`)).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();
|
||||||
|
|
||||||
const errorMsg = 'Something went wrong';
|
const errorMsg = 'Something went wrong';
|
||||||
rerender(generateJsx({ status: TxState.Error, error: new Error(errorMsg) }));
|
rerender(
|
||||||
|
generateJsx({ status: EthTxStatus.Error, error: new Error(errorMsg) })
|
||||||
|
);
|
||||||
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} failed`)).toBeInTheDocument();
|
||||||
expect(screen.getByText(errorMsg)).toBeInTheDocument();
|
expect(screen.getByText(errorMsg)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Success state waits for confirmation event if provided', () => {
|
test('Success state waits for confirmation event if provided', () => {
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
generateJsx({ status: TxState.Complete, confirmed: false })
|
generateJsx({ status: EthTxStatus.Complete, confirmed: false })
|
||||||
);
|
);
|
||||||
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
expect(screen.getByText(`${props.name} pending`)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
expect(screen.getByText('Confirmed in wallet')).toBeInTheDocument();
|
||||||
@ -98,7 +101,7 @@ test('Success state waits for confirmation event if provided', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
// @ts-ignore enforce truthy on confirmation event
|
// @ts-ignore enforce truthy on confirmation event
|
||||||
rerender(generateJsx({ confirmed: true, status: TxState.Complete }));
|
rerender(generateJsx({ confirmed: true, status: EthTxStatus.Complete }));
|
||||||
expect(
|
expect(
|
||||||
screen.queryByText('Vega is confirming your transaction...')
|
screen.queryByText('Vega is confirming your transaction...')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
@ -1,19 +1,14 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
t,
|
import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
TxState,
|
import { isExpectedEthereumError } from '../ethereum-error';
|
||||||
isExpectedEthereumError,
|
import { EthTxStatus } from '../use-ethereum-transaction';
|
||||||
} from '@vegaprotocol/react-helpers';
|
|
||||||
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
import { ConfirmRow, TxRow, ConfirmationEventRow } from './dialog-rows';
|
||||||
import { DialogWrapper } from './dialog-wrapper';
|
import { DialogWrapper } from './dialog-wrapper';
|
||||||
import { Loader } from '../loader';
|
|
||||||
import { Intent } from '../../utils/intent';
|
|
||||||
import { Dialog } from '../dialog';
|
|
||||||
import { Icon } from '../icon';
|
|
||||||
|
|
||||||
export interface TransactionDialogProps {
|
export interface TransactionDialogProps {
|
||||||
name: string;
|
name: string;
|
||||||
status: TxState;
|
status: EthTxStatus;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
confirmations: number;
|
confirmations: number;
|
||||||
txHash: string | null;
|
txHash: string | null;
|
||||||
@ -36,7 +31,7 @@ export const TransactionDialog = ({
|
|||||||
const dialogDismissed = useRef(false);
|
const dialogDismissed = useRef(false);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (status === TxState.Error) {
|
if (status === EthTxStatus.Error) {
|
||||||
return (
|
return (
|
||||||
<p className="break-all text-black dark:text-white">
|
<p className="break-all text-black dark:text-white">
|
||||||
{error && error.message}
|
{error && error.message}
|
||||||
@ -63,22 +58,22 @@ export const TransactionDialog = ({
|
|||||||
|
|
||||||
const getWrapperProps = () => {
|
const getWrapperProps = () => {
|
||||||
const propsMap = {
|
const propsMap = {
|
||||||
[TxState.Error]: {
|
[EthTxStatus.Error]: {
|
||||||
title: t(`${name} failed`),
|
title: t(`${name} failed`),
|
||||||
icon: <Icon name="warning-sign" size={20} />,
|
icon: <Icon name="warning-sign" size={20} />,
|
||||||
intent: Intent.Danger,
|
intent: Intent.Danger,
|
||||||
},
|
},
|
||||||
[TxState.Requested]: {
|
[EthTxStatus.Requested]: {
|
||||||
title: t('Confirm transaction'),
|
title: t('Confirm transaction'),
|
||||||
icon: <Icon name="hand-up" size={20} />,
|
icon: <Icon name="hand-up" size={20} />,
|
||||||
intent: Intent.Prompt,
|
intent: Intent.Prompt,
|
||||||
},
|
},
|
||||||
[TxState.Pending]: {
|
[EthTxStatus.Pending]: {
|
||||||
title: t(`${name} pending`),
|
title: t(`${name} pending`),
|
||||||
icon: <Loader size="small" />,
|
icon: <Loader size="small" />,
|
||||||
intent: Intent.Progress,
|
intent: Intent.Progress,
|
||||||
},
|
},
|
||||||
[TxState.Complete]: {
|
[EthTxStatus.Complete]: {
|
||||||
title: t(`${name} complete`),
|
title: t(`${name} complete`),
|
||||||
icon: <Icon name="tick" />,
|
icon: <Icon name="tick" />,
|
||||||
intent: Intent.Success,
|
intent: Intent.Success,
|
||||||
@ -86,7 +81,7 @@ export const TransactionDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Dialog not showing
|
// Dialog not showing
|
||||||
if (status === TxState.Default) {
|
if (status === EthTxStatus.Default) {
|
||||||
return { intent: undefined, title: '', icon: null };
|
return { intent: undefined, title: '', icon: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,11 +89,11 @@ export const TransactionDialog = ({
|
|||||||
if (confirmed !== undefined) {
|
if (confirmed !== undefined) {
|
||||||
// Vega has confirmed Tx
|
// Vega has confirmed Tx
|
||||||
if (confirmed === true) {
|
if (confirmed === true) {
|
||||||
return propsMap[TxState.Complete];
|
return propsMap[EthTxStatus.Complete];
|
||||||
}
|
}
|
||||||
// Tx is complete but still awaiting for Vega to confirm
|
// Tx is complete but still awaiting for Vega to confirm
|
||||||
else if (status === TxState.Complete) {
|
else if (status === EthTxStatus.Complete) {
|
||||||
return propsMap[TxState.Pending];
|
return propsMap[EthTxStatus.Pending];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,12 +102,12 @@ export const TransactionDialog = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Close dialog if error is due to user rejecting the tx
|
// Close dialog if error is due to user rejecting the tx
|
||||||
if (status === TxState.Error && isExpectedEthereumError(error)) {
|
if (status === EthTxStatus.Error && isExpectedEthereumError(error)) {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status !== TxState.Default && !dialogDismissed.current) {
|
if (status !== EthTxStatus.Default && !dialogDismissed.current) {
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import { renderHook, act } from '@testing-library/react-hooks/dom';
|
import { renderHook, act } from '@testing-library/react-hooks/dom';
|
||||||
import { TxState } from './use-ethereum-transaction';
|
import { EthTxStatus } from './use-ethereum-transaction';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useEthereumTransaction } from './use-ethereum-transaction';
|
import { useEthereumTransaction } from './use-ethereum-transaction';
|
||||||
import type { ethers } from 'ethers';
|
import type { ethers } from 'ethers';
|
||||||
@ -31,6 +31,7 @@ class MockContract {
|
|||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
resolve({
|
resolve({
|
||||||
|
from: 'foo',
|
||||||
confirmations: this.confirmations,
|
confirmations: this.confirmations,
|
||||||
} as ethers.ContractReceipt),
|
} as ethers.ContractReceipt),
|
||||||
100
|
100
|
||||||
@ -65,44 +66,52 @@ test('Ethereum transaction flow', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
status: TxState.Default,
|
transaction: {
|
||||||
txHash: null,
|
status: EthTxStatus.Default,
|
||||||
error: null,
|
txHash: null,
|
||||||
confirmations: 0,
|
error: null,
|
||||||
|
confirmations: 0,
|
||||||
|
receipt: null,
|
||||||
|
},
|
||||||
perform: expect.any(Function),
|
perform: expect.any(Function),
|
||||||
|
reset: expect.any(Function),
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.perform();
|
result.current.perform();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.status).toEqual(TxState.Requested);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Requested);
|
||||||
expect(result.current.confirmations).toBe(0);
|
expect(result.current.transaction.confirmations).toBe(0);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.status).toEqual(TxState.Pending);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Pending);
|
||||||
expect(result.current.txHash).toEqual(MockContract.txHash);
|
expect(result.current.transaction.txHash).toEqual(MockContract.txHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(100);
|
jest.advanceTimersByTime(100);
|
||||||
});
|
});
|
||||||
expect(result.current.confirmations).toBe(1);
|
expect(result.current.transaction.confirmations).toBe(1);
|
||||||
expect(result.current.status).toEqual(TxState.Pending);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Pending);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(100);
|
jest.advanceTimersByTime(100);
|
||||||
});
|
});
|
||||||
expect(result.current.confirmations).toBe(2);
|
expect(result.current.transaction.confirmations).toBe(2);
|
||||||
expect(result.current.status).toEqual(TxState.Pending);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Pending);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(100);
|
jest.advanceTimersByTime(100);
|
||||||
});
|
});
|
||||||
expect(result.current.confirmations).toBe(3);
|
expect(result.current.transaction.confirmations).toBe(3);
|
||||||
|
|
||||||
// Now complete as required confirmations have been surpassed
|
// Now complete as required confirmations have been surpassed
|
||||||
expect(result.current.status).toEqual(TxState.Complete);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Complete);
|
||||||
|
expect(result.current.transaction.receipt).toEqual({
|
||||||
|
from: 'foo',
|
||||||
|
confirmations: result.current.transaction.confirmations,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Error handling', async () => {
|
test('Error handling', async () => {
|
||||||
@ -116,7 +125,7 @@ test('Error handling', async () => {
|
|||||||
result.current.perform();
|
result.current.perform();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.status).toEqual(TxState.Error);
|
expect(result.current.transaction.status).toEqual(EthTxStatus.Error);
|
||||||
expect(result.current.error instanceof EthereumError).toBe(true);
|
expect(result.current.transaction.error instanceof EthereumError).toBe(true);
|
||||||
expect(result.current.error?.message).toBe(errorMsg);
|
expect(result.current.transaction.error?.message).toBe(errorMsg);
|
||||||
});
|
});
|
102
libs/web3/src/lib/use-ethereum-transaction.ts
Normal file
102
libs/web3/src/lib/use-ethereum-transaction.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import type { ethers } from 'ethers';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { EthereumError, isEthereumError } from './ethereum-error';
|
||||||
|
|
||||||
|
export enum EthTxStatus {
|
||||||
|
Default = 'Default',
|
||||||
|
Requested = 'Requested',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Complete = 'Complete',
|
||||||
|
Error = 'Error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TxError = Error | EthereumError;
|
||||||
|
|
||||||
|
export interface EthTxState {
|
||||||
|
status: EthTxStatus;
|
||||||
|
error: TxError | null;
|
||||||
|
txHash: string | null;
|
||||||
|
receipt: ethers.ContractReceipt | null;
|
||||||
|
confirmations: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
status: EthTxStatus.Default,
|
||||||
|
error: null,
|
||||||
|
txHash: null,
|
||||||
|
receipt: null,
|
||||||
|
confirmations: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEthereumTransaction = <TArgs = void>(
|
||||||
|
performTransaction: (
|
||||||
|
args: TArgs
|
||||||
|
) => Promise<ethers.ContractTransaction> | null,
|
||||||
|
requiredConfirmations = 1
|
||||||
|
) => {
|
||||||
|
const [transaction, _setTransaction] = useState<EthTxState>(initialState);
|
||||||
|
|
||||||
|
const setTransaction = useCallback((update: Partial<EthTxState>) => {
|
||||||
|
_setTransaction((curr) => ({
|
||||||
|
...curr,
|
||||||
|
...update,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const perform = useCallback(
|
||||||
|
async (args: TArgs) => {
|
||||||
|
setTransaction({
|
||||||
|
status: EthTxStatus.Requested,
|
||||||
|
error: null,
|
||||||
|
confirmations: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = performTransaction(args);
|
||||||
|
|
||||||
|
if (res === null) {
|
||||||
|
setTransaction({ status: EthTxStatus.Default });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await res;
|
||||||
|
|
||||||
|
let receipt: ethers.ContractReceipt | null = null;
|
||||||
|
|
||||||
|
setTransaction({ status: EthTxStatus.Pending, txHash: tx.hash });
|
||||||
|
|
||||||
|
for (let i = 1; i <= requiredConfirmations; i++) {
|
||||||
|
receipt = await tx.wait(i);
|
||||||
|
setTransaction({ confirmations: receipt.confirmations });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!receipt) {
|
||||||
|
throw new Error('No receipt after confirmations are met');
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransaction({ status: EthTxStatus.Complete, receipt });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setTransaction({ status: EthTxStatus.Error, error: err });
|
||||||
|
} else if (isEthereumError(err)) {
|
||||||
|
setTransaction({
|
||||||
|
status: EthTxStatus.Error,
|
||||||
|
error: new EthereumError(err.message, err.code),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTransaction({
|
||||||
|
status: EthTxStatus.Error,
|
||||||
|
error: new Error('Something went wrong'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[performTransaction, requiredConfirmations, setTransaction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setTransaction(initialState);
|
||||||
|
}, [setTransaction]);
|
||||||
|
|
||||||
|
return { perform, transaction, reset };
|
||||||
|
};
|
12
libs/withdraws/.babelrc
Normal file
12
libs/withdraws/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nrwl/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
18
libs/withdraws/.eslintrc.json
Normal file
18
libs/withdraws/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*", "__generated__"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
libs/withdraws/README.md
Normal file
7
libs/withdraws/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# withdraws
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test withdraws` to execute the unit tests via [Jest](https://jestjs.io).
|
10
libs/withdraws/jest.config.js
Normal file
10
libs/withdraws/jest.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: 'withdraws',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/libs/withdraws',
|
||||||
|
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||||
|
};
|
4
libs/withdraws/package.json
Normal file
4
libs/withdraws/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@vegaprotocol/withdraws",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
43
libs/withdraws/project.json
Normal file
43
libs/withdraws/project.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"root": "libs/withdraws",
|
||||||
|
"sourceRoot": "libs/withdraws/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nrwl/web:rollup",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/withdraws",
|
||||||
|
"tsConfig": "libs/withdraws/tsconfig.lib.json",
|
||||||
|
"project": "libs/withdraws/package.json",
|
||||||
|
"entryFile": "libs/withdraws/src/index.ts",
|
||||||
|
"external": ["react/jsx-runtime"],
|
||||||
|
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||||
|
"compiler": "babel",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "libs/withdraws/README.md",
|
||||||
|
"input": ".",
|
||||||
|
"output": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/withdraws/**/*.{ts,tsx,js,jsx}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["coverage/libs/withdraws"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/withdraws/jest.config.js",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
libs/withdraws/src/index.ts
Normal file
6
libs/withdraws/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './lib/withdraw-form';
|
||||||
|
export * from './lib/withdraw-manager';
|
||||||
|
export * from './lib/withdrawals-table';
|
||||||
|
export * from './lib/use-complete-withdraw';
|
||||||
|
export * from './lib/use-withdraw';
|
||||||
|
export * from './lib/use-withdrawals';
|
20
libs/withdraws/src/lib/__generated__/PendingWithdrawal.ts
generated
Normal file
20
libs/withdraws/src/lib/__generated__/PendingWithdrawal.ts
generated
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL fragment: PendingWithdrawal
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface PendingWithdrawal {
|
||||||
|
__typename: "Withdrawal";
|
||||||
|
/**
|
||||||
|
* Whether or the not the withdrawal is being processed on Ethereum
|
||||||
|
*/
|
||||||
|
pendingOnForeignChain: boolean;
|
||||||
|
/**
|
||||||
|
* Hash of the transaction on the foreign chain
|
||||||
|
*/
|
||||||
|
txHash: string | null;
|
||||||
|
}
|
99
libs/withdraws/src/lib/__generated__/WithdrawalEvent.ts
generated
Normal file
99
libs/withdraws/src/lib/__generated__/WithdrawalEvent.ts
generated
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { WithdrawalStatus } from "./../../../../types/src/__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL subscription operation: WithdrawalEvent
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface WithdrawalEvent_busEvents_event_TimeUpdate {
|
||||||
|
__typename: "TimeUpdate" | "MarketEvent" | "TransferResponses" | "PositionResolution" | "Order" | "Trade" | "Account" | "Party" | "MarginLevels" | "Proposal" | "Vote" | "MarketData" | "NodeSignature" | "LossSocialization" | "SettlePosition" | "Market" | "Asset" | "MarketTick" | "SettleDistressed" | "AuctionEvent" | "RiskFactor" | "Deposit" | "OracleSpec" | "LiquidityProvision";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalEvent_busEvents_event_Withdrawal_asset {
|
||||||
|
__typename: "Asset";
|
||||||
|
/**
|
||||||
|
* The id of the asset
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The symbol of the asset (e.g: GBP)
|
||||||
|
*/
|
||||||
|
symbol: string;
|
||||||
|
/**
|
||||||
|
* The precision of the asset
|
||||||
|
*/
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalEvent_busEvents_event_Withdrawal_details {
|
||||||
|
__typename: "Erc20WithdrawalDetails";
|
||||||
|
/**
|
||||||
|
* The ethereum address of the receiver of the asset funds
|
||||||
|
*/
|
||||||
|
receiverAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalEvent_busEvents_event_Withdrawal {
|
||||||
|
__typename: "Withdrawal";
|
||||||
|
/**
|
||||||
|
* The Vega internal id of the withdrawal
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The current status of the withdrawal
|
||||||
|
*/
|
||||||
|
status: WithdrawalStatus;
|
||||||
|
/**
|
||||||
|
* The amount to be withdrawn
|
||||||
|
*/
|
||||||
|
amount: string;
|
||||||
|
/**
|
||||||
|
* The asset to be withdrawn
|
||||||
|
*/
|
||||||
|
asset: WithdrawalEvent_busEvents_event_Withdrawal_asset;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was created
|
||||||
|
*/
|
||||||
|
createdTimestamp: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was finalized
|
||||||
|
*/
|
||||||
|
withdrawnTimestamp: string | null;
|
||||||
|
/**
|
||||||
|
* Hash of the transaction on the foreign chain
|
||||||
|
*/
|
||||||
|
txHash: string | null;
|
||||||
|
/**
|
||||||
|
* Foreign chain specific details about the withdrawal
|
||||||
|
*/
|
||||||
|
details: WithdrawalEvent_busEvents_event_Withdrawal_details | null;
|
||||||
|
/**
|
||||||
|
* Whether or the not the withdrawal is being processed on Ethereum
|
||||||
|
*/
|
||||||
|
pendingOnForeignChain: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WithdrawalEvent_busEvents_event = WithdrawalEvent_busEvents_event_TimeUpdate | WithdrawalEvent_busEvents_event_Withdrawal;
|
||||||
|
|
||||||
|
export interface WithdrawalEvent_busEvents {
|
||||||
|
__typename: "BusEvent";
|
||||||
|
/**
|
||||||
|
* the payload - the wrapped event
|
||||||
|
*/
|
||||||
|
event: WithdrawalEvent_busEvents_event;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalEvent {
|
||||||
|
/**
|
||||||
|
* Subscribe to event data from the event bus
|
||||||
|
*/
|
||||||
|
busEvents: WithdrawalEvent_busEvents[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalEventVariables {
|
||||||
|
partyId: string;
|
||||||
|
}
|
74
libs/withdraws/src/lib/__generated__/WithdrawalFields.ts
generated
Normal file
74
libs/withdraws/src/lib/__generated__/WithdrawalFields.ts
generated
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { WithdrawalStatus } from "./../../../../types/src/__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL fragment: WithdrawalFields
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface WithdrawalFields_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
|
||||||
|
*/
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalFields_details {
|
||||||
|
__typename: "Erc20WithdrawalDetails";
|
||||||
|
/**
|
||||||
|
* The ethereum address of the receiver of the asset funds
|
||||||
|
*/
|
||||||
|
receiverAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalFields {
|
||||||
|
__typename: "Withdrawal";
|
||||||
|
/**
|
||||||
|
* The Vega internal id of the withdrawal
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The current status of the withdrawal
|
||||||
|
*/
|
||||||
|
status: WithdrawalStatus;
|
||||||
|
/**
|
||||||
|
* The amount to be withdrawn
|
||||||
|
*/
|
||||||
|
amount: string;
|
||||||
|
/**
|
||||||
|
* The asset to be withdrawn
|
||||||
|
*/
|
||||||
|
asset: WithdrawalFields_asset;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was created
|
||||||
|
*/
|
||||||
|
createdTimestamp: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was finalized
|
||||||
|
*/
|
||||||
|
withdrawnTimestamp: string | null;
|
||||||
|
/**
|
||||||
|
* Hash of the transaction on the foreign chain
|
||||||
|
*/
|
||||||
|
txHash: string | null;
|
||||||
|
/**
|
||||||
|
* Foreign chain specific details about the withdrawal
|
||||||
|
*/
|
||||||
|
details: WithdrawalFields_details | null;
|
||||||
|
/**
|
||||||
|
* Whether or the not the withdrawal is being processed on Ethereum
|
||||||
|
*/
|
||||||
|
pendingOnForeignChain: boolean;
|
||||||
|
}
|
97
libs/withdraws/src/lib/__generated__/Withdrawals.ts
generated
Normal file
97
libs/withdraws/src/lib/__generated__/Withdrawals.ts
generated
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { WithdrawalStatus } from "./../../../../types/src/__generated__/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: Withdrawals
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Withdrawals_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
|
||||||
|
*/
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Withdrawals_party_withdrawals_details {
|
||||||
|
__typename: "Erc20WithdrawalDetails";
|
||||||
|
/**
|
||||||
|
* The ethereum address of the receiver of the asset funds
|
||||||
|
*/
|
||||||
|
receiverAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Withdrawals_party_withdrawals {
|
||||||
|
__typename: "Withdrawal";
|
||||||
|
/**
|
||||||
|
* The Vega internal id of the withdrawal
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The current status of the withdrawal
|
||||||
|
*/
|
||||||
|
status: WithdrawalStatus;
|
||||||
|
/**
|
||||||
|
* The amount to be withdrawn
|
||||||
|
*/
|
||||||
|
amount: string;
|
||||||
|
/**
|
||||||
|
* The asset to be withdrawn
|
||||||
|
*/
|
||||||
|
asset: Withdrawals_party_withdrawals_asset;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was created
|
||||||
|
*/
|
||||||
|
createdTimestamp: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time at which the withdrawal was finalized
|
||||||
|
*/
|
||||||
|
withdrawnTimestamp: string | null;
|
||||||
|
/**
|
||||||
|
* Hash of the transaction on the foreign chain
|
||||||
|
*/
|
||||||
|
txHash: string | null;
|
||||||
|
/**
|
||||||
|
* Foreign chain specific details about the withdrawal
|
||||||
|
*/
|
||||||
|
details: Withdrawals_party_withdrawals_details | null;
|
||||||
|
/**
|
||||||
|
* Whether or the not the withdrawal is being processed on Ethereum
|
||||||
|
*/
|
||||||
|
pendingOnForeignChain: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Withdrawals_party {
|
||||||
|
__typename: "Party";
|
||||||
|
/**
|
||||||
|
* Party identifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The list of all withdrawals initiated by the party
|
||||||
|
*/
|
||||||
|
withdrawals: Withdrawals_party_withdrawals[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Withdrawals {
|
||||||
|
/**
|
||||||
|
* An entity that is trading on the VEGA network
|
||||||
|
*/
|
||||||
|
party: Withdrawals_party | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalsVariables {
|
||||||
|
partyId: string;
|
||||||
|
}
|
14
libs/withdraws/src/lib/queries.ts
Normal file
14
libs/withdraws/src/lib/queries.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const ERC20_APPROVAL_QUERY = gql`
|
||||||
|
query Erc20Approval($withdrawalId: ID!) {
|
||||||
|
erc20WithdrawalApproval(withdrawalId: $withdrawalId) {
|
||||||
|
assetSource
|
||||||
|
amount
|
||||||
|
nonce
|
||||||
|
signatures
|
||||||
|
targetAddress
|
||||||
|
expiry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
58
libs/withdraws/src/lib/test-helpers.ts
Normal file
58
libs/withdraws/src/lib/test-helpers.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { AccountType, WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type { Asset, Account } from './types';
|
||||||
|
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||||
|
|
||||||
|
export const generateAsset = (override?: PartialDeep<Asset>) => {
|
||||||
|
const defaultAsset: Asset = {
|
||||||
|
id: 'asset-id',
|
||||||
|
symbol: 'asset-symbol',
|
||||||
|
name: 'asset-name',
|
||||||
|
decimals: 5,
|
||||||
|
source: {
|
||||||
|
contractAddress: 'contract-address',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return merge(defaultAsset, override);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAccount = (override?: PartialDeep<Account>) => {
|
||||||
|
const defaultAccount: Account = {
|
||||||
|
type: AccountType.General,
|
||||||
|
balance: '100000',
|
||||||
|
asset: {
|
||||||
|
id: 'asset-id',
|
||||||
|
symbol: 'asset-symbol',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return merge(defaultAccount, override);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateWithdrawal = (
|
||||||
|
override?: PartialDeep<Withdrawals_party_withdrawals>
|
||||||
|
): Withdrawals_party_withdrawals => {
|
||||||
|
return merge(
|
||||||
|
{
|
||||||
|
__typename: 'Withdrawal',
|
||||||
|
id: 'withdrawal-id',
|
||||||
|
status: WithdrawalStatus.Open,
|
||||||
|
amount: '100',
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id',
|
||||||
|
symbol: 'asset-symbol',
|
||||||
|
decimals: 2,
|
||||||
|
},
|
||||||
|
createdTimestamp: '2022-04-20T00:00:00',
|
||||||
|
withdrawnTimestamp: null,
|
||||||
|
txHash: null,
|
||||||
|
details: {
|
||||||
|
__typename: 'Erc20WithdrawalDetails',
|
||||||
|
receiverAddress: '123456___123456',
|
||||||
|
},
|
||||||
|
pendingOnForeignChain: false,
|
||||||
|
},
|
||||||
|
override
|
||||||
|
);
|
||||||
|
};
|
20
libs/withdraws/src/lib/types.ts
Normal file
20
libs/withdraws/src/lib/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { AccountType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
decimals: number;
|
||||||
|
source: {
|
||||||
|
contractAddress: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
type: AccountType;
|
||||||
|
balance: string;
|
||||||
|
asset: {
|
||||||
|
id: string;
|
||||||
|
symbol: string;
|
||||||
|
};
|
||||||
|
}
|
121
libs/withdraws/src/lib/use-complete-withdraw.spec.tsx
Normal file
121
libs/withdraws/src/lib/use-complete-withdraw.spec.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { waitFor } from '@testing-library/react';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useCompleteWithdraw } from './use-complete-withdraw';
|
||||||
|
import type {
|
||||||
|
Erc20Approval,
|
||||||
|
Erc20Approval_erc20WithdrawalApproval,
|
||||||
|
} from './__generated__/Erc20Approval';
|
||||||
|
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||||
|
import * as web3 from '@vegaprotocol/web3';
|
||||||
|
import * as sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/web3', () => ({
|
||||||
|
useBridgeContract: jest.fn(),
|
||||||
|
useEthereumTransaction: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function setup(mocks?: MockedResponse[]) {
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||||
|
);
|
||||||
|
return renderHook(() => useCompleteWithdraw(), { wrapper });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Should perform the Ethereum transaction with the fetched approval', async () => {
|
||||||
|
const withdrawalId = 'withdrawal-id';
|
||||||
|
const erc20WithdrawalApproval: Erc20Approval_erc20WithdrawalApproval = {
|
||||||
|
__typename: 'Erc20WithdrawalApproval',
|
||||||
|
assetSource: 'asset-source',
|
||||||
|
amount: '100',
|
||||||
|
nonce: '1',
|
||||||
|
signatures: 'signatures',
|
||||||
|
targetAddress: 'target-address',
|
||||||
|
expiry: 'expiry',
|
||||||
|
};
|
||||||
|
const mockERC20Approval: MockedResponse<Erc20Approval> = {
|
||||||
|
request: {
|
||||||
|
query: ERC20_APPROVAL_QUERY,
|
||||||
|
variables: { withdrawalId },
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
erc20WithdrawalApproval,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockPerform = jest.fn();
|
||||||
|
jest.spyOn(web3, 'useEthereumTransaction').mockReturnValue({
|
||||||
|
// @ts-ignore allow null transaction as its not used in hook logic
|
||||||
|
transaction: { txHash: 'tx-hash' },
|
||||||
|
perform: mockPerform,
|
||||||
|
});
|
||||||
|
const { result } = setup([mockERC20Approval]);
|
||||||
|
act(() => {
|
||||||
|
result.current.submit(withdrawalId);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPerform).toHaveBeenCalledWith(erc20WithdrawalApproval);
|
||||||
|
expect(result.current.withdrawalId).toBe(withdrawalId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Captures an error if the erc20Withdrawal is not found', async () => {
|
||||||
|
const withdrawalId = 'withdrawal-id';
|
||||||
|
const mockERC20Approval: MockedResponse<Erc20Approval> = {
|
||||||
|
request: {
|
||||||
|
query: ERC20_APPROVAL_QUERY,
|
||||||
|
variables: { withdrawalId },
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
erc20WithdrawalApproval: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockPerform = jest.fn();
|
||||||
|
const spyOnCaptureException = jest.spyOn(sentry, 'captureException');
|
||||||
|
jest.spyOn(web3, 'useEthereumTransaction').mockReturnValue({
|
||||||
|
// @ts-ignore allow null transaction as its not used in hook logic
|
||||||
|
transaction: { txHash: 'tx-hash' },
|
||||||
|
perform: mockPerform,
|
||||||
|
});
|
||||||
|
const { result } = setup([mockERC20Approval]);
|
||||||
|
act(() => {
|
||||||
|
result.current.submit(withdrawalId);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPerform).not.toHaveBeenCalled();
|
||||||
|
expect(spyOnCaptureException).toHaveBeenCalled();
|
||||||
|
expect(result.current.withdrawalId).toBe(withdrawalId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Captures an error if erc20 approval query fails', async () => {
|
||||||
|
const withdrawalId = 'withdrawal-id';
|
||||||
|
const mockERC20Approval: MockedResponse<Erc20Approval> = {
|
||||||
|
request: {
|
||||||
|
query: ERC20_APPROVAL_QUERY,
|
||||||
|
variables: { withdrawalId },
|
||||||
|
},
|
||||||
|
error: new Error('query failed'),
|
||||||
|
};
|
||||||
|
const mockPerform = jest.fn();
|
||||||
|
const spyOnCaptureException = jest.spyOn(sentry, 'captureException');
|
||||||
|
jest.spyOn(web3, 'useEthereumTransaction').mockReturnValue({
|
||||||
|
// @ts-ignore allow null transaction as its not used in hook logic
|
||||||
|
transaction: { txHash: 'tx-hash' },
|
||||||
|
perform: mockPerform,
|
||||||
|
});
|
||||||
|
const { result } = setup([mockERC20Approval]);
|
||||||
|
act(() => {
|
||||||
|
result.current.submit(withdrawalId);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockPerform).not.toHaveBeenCalled();
|
||||||
|
expect(spyOnCaptureException).toHaveBeenCalled();
|
||||||
|
expect(result.current.withdrawalId).toBe(withdrawalId);
|
||||||
|
});
|
||||||
|
});
|
75
libs/withdraws/src/lib/use-complete-withdraw.ts
Normal file
75
libs/withdraws/src/lib/use-complete-withdraw.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
|
import { captureException } from '@sentry/nextjs';
|
||||||
|
import { useBridgeContract, useEthereumTransaction } from '@vegaprotocol/web3';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { ERC20_APPROVAL_QUERY } from './queries';
|
||||||
|
import type {
|
||||||
|
Erc20Approval,
|
||||||
|
Erc20ApprovalVariables,
|
||||||
|
} from './__generated__/Erc20Approval';
|
||||||
|
import type { PendingWithdrawal } from './__generated__/PendingWithdrawal';
|
||||||
|
|
||||||
|
export const PENDING_WITHDRAWAL_FRAGMMENT = gql`
|
||||||
|
fragment PendingWithdrawal on Withdrawal {
|
||||||
|
pendingOnForeignChain @client
|
||||||
|
txHash
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface WithdrawTransactionArgs {
|
||||||
|
assetSource: string;
|
||||||
|
amount: string;
|
||||||
|
nonce: string;
|
||||||
|
signatures: string;
|
||||||
|
targetAddress: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCompleteWithdraw = () => {
|
||||||
|
const { query, cache } = useApolloClient();
|
||||||
|
const contract = useBridgeContract();
|
||||||
|
const [id, setId] = useState('');
|
||||||
|
const { transaction, perform } =
|
||||||
|
useEthereumTransaction<WithdrawTransactionArgs>((args) => {
|
||||||
|
if (!contract) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return contract.withdraw(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = useCallback(
|
||||||
|
async (withdrawalId: string) => {
|
||||||
|
setId(withdrawalId);
|
||||||
|
try {
|
||||||
|
const res = await query<Erc20Approval, Erc20ApprovalVariables>({
|
||||||
|
query: ERC20_APPROVAL_QUERY,
|
||||||
|
variables: { withdrawalId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.data.erc20WithdrawalApproval) {
|
||||||
|
throw new Error('Could not retrieve withdrawal approval');
|
||||||
|
}
|
||||||
|
|
||||||
|
perform(res.data.erc20WithdrawalApproval);
|
||||||
|
} catch (err) {
|
||||||
|
captureException(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[query, perform]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && transaction.txHash) {
|
||||||
|
cache.writeFragment<PendingWithdrawal>({
|
||||||
|
id: `Withdrawal:${id}`,
|
||||||
|
fragment: PENDING_WITHDRAWAL_FRAGMMENT,
|
||||||
|
data: {
|
||||||
|
__typename: 'Withdrawal',
|
||||||
|
pendingOnForeignChain: true,
|
||||||
|
txHash: transaction.txHash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [cache, transaction.txHash, id]);
|
||||||
|
|
||||||
|
return { transaction, submit, withdrawalId: id };
|
||||||
|
};
|
179
libs/withdraws/src/lib/use-withdraw.spec.tsx
Normal file
179
libs/withdraws/src/lib/use-withdraw.spec.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { Erc20Approval } from './__generated__/Erc20Approval';
|
||||||
|
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';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/web3', () => ({
|
||||||
|
useBridgeContract: jest.fn(),
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delay: 5000,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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
|
||||||
|
);
|
||||||
|
expect(mockPerform).toHaveBeenCalledWith(
|
||||||
|
// @ts-ignore MockedRespones types inteferring
|
||||||
|
mockERC20Approval.result.data.erc20WithdrawalApproval
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reset will reset both transactions', async () => {
|
||||||
|
const { result } = setup([mockERC20Approval]);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockEthReset).toHaveBeenCalled();
|
||||||
|
expect(mockVegaReset).toHaveBeenCalled();
|
||||||
|
});
|
107
libs/withdraws/src/lib/use-withdraw.ts
Normal file
107
libs/withdraws/src/lib/use-withdraw.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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 { WithdrawTransactionArgs } from './use-complete-withdraw';
|
||||||
|
import type {
|
||||||
|
Erc20Approval,
|
||||||
|
Erc20ApprovalVariables,
|
||||||
|
Erc20Approval_erc20WithdrawalApproval,
|
||||||
|
} from './__generated__/Erc20Approval';
|
||||||
|
|
||||||
|
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<WithdrawTransactionArgs>((args) => {
|
||||||
|
if (!contract) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return contract.withdraw(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
stopPolling();
|
||||||
|
setApproval(data.erc20WithdrawalApproval);
|
||||||
|
}
|
||||||
|
}, [data, stopPolling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (approval && !cancelled) {
|
||||||
|
perform(approval);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [approval]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
resetVegaTx();
|
||||||
|
resetEthTx();
|
||||||
|
}, [resetVegaTx, resetEthTx]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
submit,
|
||||||
|
reset,
|
||||||
|
approval,
|
||||||
|
withdrawalId,
|
||||||
|
vegaTx,
|
||||||
|
ethTx,
|
||||||
|
};
|
||||||
|
};
|
127
libs/withdraws/src/lib/use-withdrawals.spec.tsx
Normal file
127
libs/withdraws/src/lib/use-withdrawals.spec.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
|
import { generateWithdrawal } from './test-helpers';
|
||||||
|
import { updateQuery } from './use-withdrawals';
|
||||||
|
import type {
|
||||||
|
WithdrawalEvent,
|
||||||
|
WithdrawalEvent_busEvents_event_Withdrawal,
|
||||||
|
} from './__generated__/WithdrawalEvent';
|
||||||
|
import type {
|
||||||
|
Withdrawals,
|
||||||
|
Withdrawals_party_withdrawals,
|
||||||
|
} from './__generated__/Withdrawals';
|
||||||
|
|
||||||
|
describe('updateQuery', () => {
|
||||||
|
test('Updates existing withdrawals', () => {
|
||||||
|
const withdrawal = generateWithdrawal({
|
||||||
|
id: '1',
|
||||||
|
status: WithdrawalStatus.Open,
|
||||||
|
});
|
||||||
|
const withdrawalUpdate = generateWithdrawal({
|
||||||
|
id: '1',
|
||||||
|
status: WithdrawalStatus.Finalized,
|
||||||
|
});
|
||||||
|
const prev = mockQuery([withdrawal]);
|
||||||
|
const incoming = mockSub([withdrawalUpdate]);
|
||||||
|
|
||||||
|
expect(updateQuery(prev, incoming)).toEqual({
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'party-id',
|
||||||
|
withdrawals: [withdrawalUpdate],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Adds new withdrawals', () => {
|
||||||
|
const withdrawal = generateWithdrawal({
|
||||||
|
id: '1',
|
||||||
|
amount: '100',
|
||||||
|
});
|
||||||
|
const withdrawalUpdate = generateWithdrawal({
|
||||||
|
id: '2',
|
||||||
|
amount: '200',
|
||||||
|
});
|
||||||
|
const prev = mockQuery([withdrawal]);
|
||||||
|
const incoming = mockSub([withdrawalUpdate]);
|
||||||
|
|
||||||
|
expect(updateQuery(prev, incoming)).toEqual({
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'party-id',
|
||||||
|
withdrawals: [withdrawalUpdate, withdrawal],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Creates new party if not present', () => {
|
||||||
|
const withdrawalUpdate = generateWithdrawal({
|
||||||
|
id: '2',
|
||||||
|
});
|
||||||
|
const incoming = mockSub([withdrawalUpdate]);
|
||||||
|
|
||||||
|
expect(updateQuery({ party: null }, incoming)).toEqual({
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
withdrawals: [withdrawalUpdate],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Handles updates and inserts simultaneously', () => {
|
||||||
|
const withdrawal1 = generateWithdrawal({
|
||||||
|
id: '1',
|
||||||
|
status: WithdrawalStatus.Open,
|
||||||
|
});
|
||||||
|
const withdrawal2 = generateWithdrawal({
|
||||||
|
id: '2',
|
||||||
|
});
|
||||||
|
const withdrawalUpdate = generateWithdrawal({
|
||||||
|
id: '1',
|
||||||
|
status: WithdrawalStatus.Finalized,
|
||||||
|
});
|
||||||
|
const withdrawalNew = generateWithdrawal({
|
||||||
|
id: '3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const prev = mockQuery([withdrawal1, withdrawal2]);
|
||||||
|
const incoming = mockSub([withdrawalUpdate, withdrawalNew]);
|
||||||
|
expect(updateQuery(prev, incoming)).toEqual({
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'party-id',
|
||||||
|
withdrawals: [withdrawalUpdate, withdrawalNew, withdrawal2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockQuery = (
|
||||||
|
withdrawals: Withdrawals_party_withdrawals[]
|
||||||
|
): Withdrawals => {
|
||||||
|
return {
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'party-id',
|
||||||
|
withdrawals,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSub = (
|
||||||
|
withdrawals: WithdrawalEvent_busEvents_event_Withdrawal[]
|
||||||
|
): {
|
||||||
|
subscriptionData: {
|
||||||
|
data: WithdrawalEvent;
|
||||||
|
};
|
||||||
|
} => {
|
||||||
|
return {
|
||||||
|
subscriptionData: {
|
||||||
|
data: {
|
||||||
|
busEvents: withdrawals.map((w) => ({
|
||||||
|
__typename: 'BusEvent',
|
||||||
|
event: w,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
144
libs/withdraws/src/lib/use-withdrawals.ts
Normal file
144
libs/withdraws/src/lib/use-withdrawals.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import uniqBy from 'lodash/uniqBy';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import type {
|
||||||
|
WithdrawalEvent,
|
||||||
|
WithdrawalEventVariables,
|
||||||
|
WithdrawalEvent_busEvents_event,
|
||||||
|
WithdrawalEvent_busEvents_event_Withdrawal,
|
||||||
|
} from './__generated__/WithdrawalEvent';
|
||||||
|
import type {
|
||||||
|
Withdrawals,
|
||||||
|
WithdrawalsVariables,
|
||||||
|
} from './__generated__/Withdrawals';
|
||||||
|
|
||||||
|
const WITHDRAWAL_FRAGMENT = gql`
|
||||||
|
fragment WithdrawalFields on Withdrawal {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
amount
|
||||||
|
asset {
|
||||||
|
id
|
||||||
|
symbol
|
||||||
|
decimals
|
||||||
|
}
|
||||||
|
createdTimestamp
|
||||||
|
withdrawnTimestamp
|
||||||
|
txHash
|
||||||
|
details {
|
||||||
|
... on Erc20WithdrawalDetails {
|
||||||
|
receiverAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingOnForeignChain @client
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WITHDRAWALS_QUERY = gql`
|
||||||
|
${WITHDRAWAL_FRAGMENT}
|
||||||
|
query Withdrawals($partyId: ID!) {
|
||||||
|
party(id: $partyId) {
|
||||||
|
id
|
||||||
|
withdrawals {
|
||||||
|
...WithdrawalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WITHDRAWAL_BUS_EVENT_SUB = gql`
|
||||||
|
${WITHDRAWAL_FRAGMENT}
|
||||||
|
subscription WithdrawalEvent($partyId: ID!) {
|
||||||
|
busEvents(partyId: $partyId, batchSize: 0, types: [Withdrawal]) {
|
||||||
|
event {
|
||||||
|
... on Withdrawal {
|
||||||
|
...WithdrawalFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const useWithdrawals = () => {
|
||||||
|
const { keypair } = useVegaWallet();
|
||||||
|
const { subscribeToMore, ...queryResult } = useQuery<
|
||||||
|
Withdrawals,
|
||||||
|
WithdrawalsVariables
|
||||||
|
>(WITHDRAWALS_QUERY, {
|
||||||
|
variables: { partyId: keypair?.pub || '' },
|
||||||
|
skip: !keypair?.pub,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!keypair?.pub) return;
|
||||||
|
|
||||||
|
const unsub = subscribeToMore<WithdrawalEvent, WithdrawalEventVariables>({
|
||||||
|
document: WITHDRAWAL_BUS_EVENT_SUB,
|
||||||
|
variables: { partyId: keypair.pub },
|
||||||
|
updateQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub();
|
||||||
|
};
|
||||||
|
}, [keypair?.pub, subscribeToMore]);
|
||||||
|
|
||||||
|
return queryResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateQuery: UpdateQueryFn<
|
||||||
|
Withdrawals,
|
||||||
|
WithdrawalEventVariables,
|
||||||
|
WithdrawalEvent
|
||||||
|
> = (prev, { subscriptionData }) => {
|
||||||
|
if (!subscriptionData.data.busEvents?.length) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curr = prev.party?.withdrawals || [];
|
||||||
|
const incoming = subscriptionData.data.busEvents
|
||||||
|
.map((e) => {
|
||||||
|
return {
|
||||||
|
...e.event,
|
||||||
|
pendingOnForeignChain: false,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
isWithdrawalEvent
|
||||||
|
// Need this type cast here, TS can't infer that we've filtered any event types
|
||||||
|
// that arent Withdrawals
|
||||||
|
) as WithdrawalEvent_busEvents_event_Withdrawal[];
|
||||||
|
|
||||||
|
const withdrawals = uniqBy([...incoming, ...curr], 'id');
|
||||||
|
|
||||||
|
// Write new party to cache if not present
|
||||||
|
if (!prev.party) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
withdrawals,
|
||||||
|
},
|
||||||
|
} as Withdrawals;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
party: {
|
||||||
|
...prev.party,
|
||||||
|
withdrawals,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWithdrawalEvent = (
|
||||||
|
event: WithdrawalEvent_busEvents_event
|
||||||
|
): event is WithdrawalEvent_busEvents_event_Withdrawal => {
|
||||||
|
if (event.__typename === 'Withdrawal') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
112
libs/withdraws/src/lib/withdraw-dialog.spec.tsx
Normal file
112
libs/withdraws/src/lib/withdraw-dialog.spec.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
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} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('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)
|
||||||
|
);
|
||||||
|
});
|
178
libs/withdraws/src/lib/withdraw-dialog.tsx
Normal file
178
libs/withdraws/src/lib/withdraw-dialog.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
EtherscanLink,
|
||||||
|
Icon,
|
||||||
|
Intent,
|
||||||
|
Loader,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||||
|
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { EthTxState } 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 { intent, ...props } = getProps(approval, vegaTx, ethTx);
|
||||||
|
return (
|
||||||
|
<Dialog open={dialogOpen} intent={intent} onChange={onDialogChange}>
|
||||||
|
<DialogWrapper {...props} />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DialogWrapperProps {
|
||||||
|
children: ReactNode;
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DialogWrapper = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
}: DialogWrapperProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-12 max-w-full text-ui">
|
||||||
|
<div className="pt-8 fill-current">{icon}</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-h4 text-black dark:text-white capitalize mb-12">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StepProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step = ({ children }: StepProps) => {
|
||||||
|
return <p 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
|
||||||
|
) => {
|
||||||
|
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" size={20} />,
|
||||||
|
intent: Intent.Danger,
|
||||||
|
children: (
|
||||||
|
<Step>
|
||||||
|
{vegaTx.error && (
|
||||||
|
<pre className="text-ui break-all whitespace-pre-wrap">
|
||||||
|
{JSON.stringify(vegaTx.error, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Step>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[VegaTxStatus.Requested]: {
|
||||||
|
title: t('Confirm withdrawal'),
|
||||||
|
icon: <Icon name="hand-up" size={20} />,
|
||||||
|
intent: Intent.Prompt,
|
||||||
|
children: <Step>Confirm withdrawal in Vega wallet</Step>,
|
||||||
|
},
|
||||||
|
[VegaTxStatus.Pending]: {
|
||||||
|
title: t('Withdrawal transaction pending'),
|
||||||
|
icon: <Loader size="small" />,
|
||||||
|
intent: Intent.Progress,
|
||||||
|
children: <Step>Awaiting transaction</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" size={20} />,
|
||||||
|
intent: Intent.Danger,
|
||||||
|
children: (
|
||||||
|
<Step>
|
||||||
|
{ethTx.error ? ethTx.error.message : t('Something went wrong')}
|
||||||
|
</Step>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[EthTxStatus.Requested]: {
|
||||||
|
title: t('Confirm transaction'),
|
||||||
|
icon: <Icon name="hand-up" size={20} />,
|
||||||
|
intent: Intent.Prompt,
|
||||||
|
children: <Step>{t('Confirm transaction in wallet')}</Step>,
|
||||||
|
},
|
||||||
|
[EthTxStatus.Pending]: {
|
||||||
|
title: t('Ethereum transaction pending'),
|
||||||
|
icon: <Loader size="small" />,
|
||||||
|
intent: Intent.Progress,
|
||||||
|
children: (
|
||||||
|
<Step>
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
`Awaiting Ethereum transaction ${ethTx.confirmations}/1 confirmations...`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<EtherscanLink
|
||||||
|
tx={ethTx.txHash || ''}
|
||||||
|
className="text-vega-pink dark:text-vega-yellow"
|
||||||
|
text={t('View on Etherscan')}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
[EthTxStatus.Complete]: {
|
||||||
|
title: t('Withdrawal complete'),
|
||||||
|
icon: <Icon name="tick" />,
|
||||||
|
intent: Intent.Success,
|
||||||
|
children: (
|
||||||
|
<Step>
|
||||||
|
<span>{t('Ethereum transaction complete')}</span>
|
||||||
|
<EtherscanLink
|
||||||
|
tx={ethTx.txHash || ''}
|
||||||
|
className="text-vega-pink dark:text-vega-yellow"
|
||||||
|
text={t('View on Etherscan')}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return approval ? ethTxPropsMap[ethTx.status] : vegaTxPropsMap[vegaTx.status];
|
||||||
|
};
|
108
libs/withdraws/src/lib/withdraw-form.spec.tsx
Normal file
108
libs/withdraws/src/lib/withdraw-form.spec.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { WithdrawForm } from './withdraw-form';
|
||||||
|
import type { WithdrawFormProps } from './withdraw-form';
|
||||||
|
import { generateAsset } from './test-helpers';
|
||||||
|
|
||||||
|
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||||
|
let props: WithdrawFormProps;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const assets = [
|
||||||
|
generateAsset(),
|
||||||
|
generateAsset({
|
||||||
|
id: 'asset-id-2',
|
||||||
|
symbol: 'asset-symbol-2',
|
||||||
|
name: 'asset-name-2',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
props = {
|
||||||
|
assets,
|
||||||
|
min: new BigNumber(0.00001),
|
||||||
|
max: new BigNumber(100),
|
||||||
|
ethereumAccount: undefined,
|
||||||
|
selectedAsset: undefined,
|
||||||
|
onSelectAsset: jest.fn(),
|
||||||
|
submitWithdraw: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateJsx = (props: WithdrawFormProps) => <WithdrawForm {...props} />;
|
||||||
|
|
||||||
|
test('Validation', async () => {
|
||||||
|
const { rerender } = render(generateJsx(props));
|
||||||
|
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
|
||||||
|
expect(await screen.findAllByRole('alert')).toHaveLength(3);
|
||||||
|
expect(screen.getAllByText('Required')).toHaveLength(3);
|
||||||
|
|
||||||
|
// Selected asset state lives in state so rerender with it now selected
|
||||||
|
rerender(generateJsx({ ...props, selectedAsset: props.assets[0] }));
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||||
|
target: { value: props.assets[0].id },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||||
|
target: { value: 'invalid-address' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '101' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText('Invalid Ethereum address')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Value is above maximum')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||||
|
target: { value: ethereumAddress },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '0.000000000001' },
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
|
||||||
|
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '40' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.submitWithdraw).toHaveBeenCalledWith({
|
||||||
|
asset: props.assets[0].id,
|
||||||
|
amount: '4000000',
|
||||||
|
receiverAddress: ethereumAddress,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Use max button', () => {
|
||||||
|
const asset = props.assets[0];
|
||||||
|
render(generateJsx({ ...props, selectedAsset: asset }));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Use maximum'));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('Amount')).toHaveValue(
|
||||||
|
Number(props.max.toFixed(asset.decimals))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Use connected Ethereum account', () => {
|
||||||
|
render(generateJsx({ ...props, ethereumAccount: ethereumAddress }));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Use connected'));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('To (Ethereum address)')).toHaveValue(
|
||||||
|
ethereumAddress
|
||||||
|
);
|
||||||
|
});
|
184
libs/withdraws/src/lib/withdraw-form.tsx
Normal file
184
libs/withdraws/src/lib/withdraw-form.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import {
|
||||||
|
ethereumAddress,
|
||||||
|
maxSafe,
|
||||||
|
minSafe,
|
||||||
|
t,
|
||||||
|
removeDecimal,
|
||||||
|
required,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
Input,
|
||||||
|
InputError,
|
||||||
|
Select,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type BigNumber from 'bignumber.js';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import type { WithdrawalFields } from './use-withdraw';
|
||||||
|
import type { Asset } from './types';
|
||||||
|
|
||||||
|
interface FormFields {
|
||||||
|
asset: string;
|
||||||
|
to: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawFormProps {
|
||||||
|
assets: Asset[];
|
||||||
|
max: BigNumber;
|
||||||
|
min: BigNumber;
|
||||||
|
selectedAsset?: Asset;
|
||||||
|
ethereumAccount?: string;
|
||||||
|
onSelectAsset: (assetId: string) => void;
|
||||||
|
submitWithdraw: (withdrawal: WithdrawalFields) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithdrawForm = ({
|
||||||
|
assets,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
selectedAsset,
|
||||||
|
ethereumAccount,
|
||||||
|
onSelectAsset,
|
||||||
|
submitWithdraw,
|
||||||
|
}: WithdrawFormProps) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
clearErrors,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormFields>({
|
||||||
|
defaultValues: {
|
||||||
|
asset: selectedAsset?.id,
|
||||||
|
to: ethereumAccount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const onSubmit = async (fields: FormFields) => {
|
||||||
|
if (!selectedAsset) {
|
||||||
|
throw new Error('Asset not selected');
|
||||||
|
}
|
||||||
|
submitWithdraw({
|
||||||
|
asset: selectedAsset.id,
|
||||||
|
amount: removeDecimal(fields.amount, selectedAsset.decimals),
|
||||||
|
receiverAddress: fields.to,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
noValidate={true}
|
||||||
|
data-testid="withdraw-form"
|
||||||
|
>
|
||||||
|
<FormGroup label={t('Asset')} labelFor="asset" className="relative">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="asset"
|
||||||
|
rules={{ validate: { required } }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
onSelectAsset(e.target.value);
|
||||||
|
field.onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
value={selectedAsset?.id || ''}
|
||||||
|
id="asset"
|
||||||
|
>
|
||||||
|
<option value="">{t('Please select')}</option>
|
||||||
|
{assets.map((a) => (
|
||||||
|
<option key={a.id} value={a.id}>
|
||||||
|
{a.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errors.asset?.message && (
|
||||||
|
<InputError intent="danger" className="mt-4">
|
||||||
|
{errors.asset.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={t('To (Ethereum address)')}
|
||||||
|
labelFor="ethereum-address"
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('to', { validate: { required, ethereumAddress } })}
|
||||||
|
id="ethereum-address"
|
||||||
|
/>
|
||||||
|
{errors.to?.message && (
|
||||||
|
<InputError intent="danger" className="mt-4">
|
||||||
|
{errors.to.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
{ethereumAccount && (
|
||||||
|
<UseButton
|
||||||
|
onClick={() => {
|
||||||
|
setValue('to', ethereumAccount);
|
||||||
|
clearErrors('to');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Use connected')}
|
||||||
|
</UseButton>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
autoComplete="off"
|
||||||
|
id="amount"
|
||||||
|
{...register('amount', {
|
||||||
|
validate: {
|
||||||
|
required,
|
||||||
|
maxSafe: (value) => maxSafe(max)(value),
|
||||||
|
minSafe: (value) => minSafe(min)(value),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.amount?.message && (
|
||||||
|
<InputError intent="danger" className="mt-4">
|
||||||
|
{errors.amount.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
{selectedAsset && (
|
||||||
|
<UseButton
|
||||||
|
onClick={() => {
|
||||||
|
setValue('amount', max.toFixed(selectedAsset.decimals));
|
||||||
|
clearErrors('amount');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Use maximum')}
|
||||||
|
</UseButton>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseButtonProps {
|
||||||
|
children: ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UseButton = ({ children, onClick }: UseButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-auto text-ui absolute top-0 right-0 underline"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
127
libs/withdraws/src/lib/withdraw-manager.spec.tsx
Normal file
127
libs/withdraws/src/lib/withdraw-manager.spec.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { generateAccount, generateAsset } from './test-helpers';
|
||||||
|
import type { WithdrawManagerProps } from './withdraw-manager';
|
||||||
|
import { WithdrawManager } from './withdraw-manager';
|
||||||
|
import * as withdrawHook from './use-withdraw';
|
||||||
|
import { initialState as vegaTxInitialState } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
EthereumError,
|
||||||
|
EthTxStatus,
|
||||||
|
initialState as ethTxInitialState,
|
||||||
|
} from '@vegaprotocol/web3';
|
||||||
|
|
||||||
|
const ethereumAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||||
|
|
||||||
|
jest.mock('@web3-react/core', () => ({
|
||||||
|
useWeb3React: () => ({ account: ethereumAddress }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let props: WithdrawManagerProps;
|
||||||
|
let useWithdrawValue: ReturnType<typeof withdrawHook.useWithdraw>;
|
||||||
|
let useWithdraw: jest.SpyInstance;
|
||||||
|
let mockSubmit: jest.Mock;
|
||||||
|
let mockReset: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
assets: [generateAsset()],
|
||||||
|
accounts: [generateAccount()],
|
||||||
|
initialAssetId: undefined,
|
||||||
|
};
|
||||||
|
mockSubmit = jest.fn();
|
||||||
|
mockReset = jest.fn();
|
||||||
|
useWithdrawValue = {
|
||||||
|
ethTx: ethTxInitialState,
|
||||||
|
vegaTx: vegaTxInitialState,
|
||||||
|
approval: null,
|
||||||
|
withdrawalId: null,
|
||||||
|
submit: mockSubmit,
|
||||||
|
reset: mockReset,
|
||||||
|
};
|
||||||
|
useWithdraw = jest
|
||||||
|
.spyOn(withdrawHook, 'useWithdraw')
|
||||||
|
.mockReturnValue(useWithdrawValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateJsx = (props: WithdrawManagerProps) => (
|
||||||
|
<WithdrawManager {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
test('Valid form submission opens transaction dialog', async () => {
|
||||||
|
render(generateJsx(props));
|
||||||
|
submitValid();
|
||||||
|
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||||
|
expect(mockReset).toHaveBeenCalled();
|
||||||
|
expect(mockSubmit).toHaveBeenCalledWith({
|
||||||
|
amount: '1000',
|
||||||
|
asset: props.assets[0].id,
|
||||||
|
receiverAddress: ethereumAddress,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Expected Ethereum error closes the dialog', async () => {
|
||||||
|
const { rerender } = render(generateJsx(props));
|
||||||
|
submitValid();
|
||||||
|
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||||
|
useWithdraw.mockReturnValue({
|
||||||
|
...useWithdrawValue,
|
||||||
|
ethTx: {
|
||||||
|
...useWithdrawValue.ethTx,
|
||||||
|
status: EthTxStatus.Error,
|
||||||
|
error: new EthereumError('User rejected transaction', 4001),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
rerender(generateJsx(props));
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Correct min max values provided to form', async () => {
|
||||||
|
render(generateJsx(props));
|
||||||
|
|
||||||
|
// Set other fields to be valid
|
||||||
|
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||||
|
target: { value: props.assets[0].id },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||||
|
target: { value: ethereumAddress },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Min amount
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '0.00000001' },
|
||||||
|
});
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
expect(await screen.findByText('Value is below minimum')).toBeInTheDocument();
|
||||||
|
expect(mockSubmit).not.toBeCalled();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '0.00001' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Max amount (balance is 1)
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '2' },
|
||||||
|
});
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
expect(await screen.findByText('Value is above maximum'));
|
||||||
|
expect(mockSubmit).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial asset id can preselect asset', async () => {
|
||||||
|
const asset = props.assets[0];
|
||||||
|
render(generateJsx({ ...props, initialAssetId: asset.id }));
|
||||||
|
expect(screen.getByLabelText('Asset')).toHaveValue(asset.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitValid = () => {
|
||||||
|
fireEvent.change(screen.getByLabelText('Asset'), {
|
||||||
|
target: { value: props.assets[0].id },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||||
|
target: { value: ethereumAddress },
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '0.01' },
|
||||||
|
});
|
||||||
|
fireEvent.submit(screen.getByTestId('withdraw-form'));
|
||||||
|
};
|
108
libs/withdraws/src/lib/withdraw-manager.tsx
Normal file
108
libs/withdraws/src/lib/withdraw-manager.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { WithdrawForm } from './withdraw-form';
|
||||||
|
import type { WithdrawalFields } from './use-withdraw';
|
||||||
|
import { useWithdraw } from './use-withdraw';
|
||||||
|
import { WithdrawDialog } from './withdraw-dialog';
|
||||||
|
import { isExpectedEthereumError, EthTxStatus } from '@vegaprotocol/web3';
|
||||||
|
import { addDecimal } from '@vegaprotocol/react-helpers';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import type { Account, Asset } from './types';
|
||||||
|
import { useWeb3React } from '@web3-react/core';
|
||||||
|
|
||||||
|
export interface WithdrawManagerProps {
|
||||||
|
assets: Asset[];
|
||||||
|
accounts: Account[];
|
||||||
|
initialAssetId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithdrawManager = ({
|
||||||
|
assets,
|
||||||
|
accounts,
|
||||||
|
initialAssetId,
|
||||||
|
}: WithdrawManagerProps) => {
|
||||||
|
const dialogDismissed = useRef(false);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
|
||||||
|
|
||||||
|
const { account: ethereumAccount } = useWeb3React();
|
||||||
|
const { ethTx, vegaTx, approval, submit, reset } = useWithdraw(
|
||||||
|
dialogDismissed.current
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the asset object from the select box
|
||||||
|
const asset = useMemo(() => {
|
||||||
|
return assets?.find((a) => a.id === assetId);
|
||||||
|
}, [assets, assetId]);
|
||||||
|
|
||||||
|
const max = useMemo(() => {
|
||||||
|
if (!asset) {
|
||||||
|
return new BigNumber(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = accounts.find(
|
||||||
|
(a) => a.type === AccountType.General && a.asset.id === asset.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return new BigNumber(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = new BigNumber(addDecimal(account.balance, asset.decimals));
|
||||||
|
return v;
|
||||||
|
}, [asset, accounts]);
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close dialog if error is due to user rejecting the tx
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
ethTx.status === EthTxStatus.Error &&
|
||||||
|
isExpectedEthereumError(ethTx.error)
|
||||||
|
) {
|
||||||
|
setDialogOpen(false);
|
||||||
|
}
|
||||||
|
}, [ethTx.status, ethTx.error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WithdrawForm
|
||||||
|
ethereumAccount={ethereumAccount}
|
||||||
|
selectedAsset={asset}
|
||||||
|
onSelectAsset={(id) => setAssetId(id)}
|
||||||
|
assets={sortBy(assets, 'name')}
|
||||||
|
max={max}
|
||||||
|
min={min}
|
||||||
|
submitWithdraw={handleSubmit}
|
||||||
|
/>
|
||||||
|
<WithdrawDialog
|
||||||
|
vegaTx={vegaTx}
|
||||||
|
ethTx={ethTx}
|
||||||
|
approval={approval}
|
||||||
|
dialogOpen={dialogOpen}
|
||||||
|
onDialogChange={(isOpen) => {
|
||||||
|
setDialogOpen(isOpen);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
dialogDismissed.current = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
114
libs/withdraws/src/lib/withdrawals-table.spec.tsx
Normal file
114
libs/withdraws/src/lib/withdrawals-table.spec.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||||
|
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
|
import { generateWithdrawal } from './test-helpers';
|
||||||
|
import type {
|
||||||
|
StatusCellProps,
|
||||||
|
WithdrawalsTableProps,
|
||||||
|
} from './withdrawals-table';
|
||||||
|
import { StatusCell } from './withdrawals-table';
|
||||||
|
import { WithdrawalsTable } from './withdrawals-table';
|
||||||
|
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||||
|
|
||||||
|
jest.mock('@web3-react/core', () => ({
|
||||||
|
useWeb3React: () => ({ provider: undefined }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const generateJsx = (props: WithdrawalsTableProps) => (
|
||||||
|
<MockedProvider>
|
||||||
|
<WithdrawalsTable {...props} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
test('Renders the correct columns', async () => {
|
||||||
|
const withdrawal = generateWithdrawal();
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ withdrawals: [withdrawal] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = screen.getAllByRole('columnheader');
|
||||||
|
expect(headers).toHaveLength(5);
|
||||||
|
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||||
|
'Asset',
|
||||||
|
'Amount',
|
||||||
|
'Recipient',
|
||||||
|
'Created at',
|
||||||
|
'Status',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cells = screen.getAllByRole('gridcell');
|
||||||
|
const expectedValues = [
|
||||||
|
'asset-symbol',
|
||||||
|
formatNumber(withdrawal.amount, withdrawal.asset.decimals),
|
||||||
|
'123456\u2026123456',
|
||||||
|
getDateTimeFormat().format(new Date(withdrawal.createdTimestamp)),
|
||||||
|
withdrawal.status,
|
||||||
|
];
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('StatusCell', () => {
|
||||||
|
let props: StatusCellProps;
|
||||||
|
let withdrawal: Withdrawals_party_withdrawals;
|
||||||
|
let mockComplete: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
withdrawal = generateWithdrawal();
|
||||||
|
mockComplete = jest.fn();
|
||||||
|
// @ts-ignore dont need full ICellRendererParams
|
||||||
|
props = {
|
||||||
|
value: withdrawal.status,
|
||||||
|
data: withdrawal,
|
||||||
|
complete: mockComplete,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Open', () => {
|
||||||
|
props.value = WithdrawalStatus.Finalized;
|
||||||
|
props.data.pendingOnForeignChain = false;
|
||||||
|
props.data.txHash = null;
|
||||||
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Open'));
|
||||||
|
fireEvent.click(screen.getByText('Complete', { selector: 'button' }));
|
||||||
|
expect(mockComplete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pending', () => {
|
||||||
|
props.value = WithdrawalStatus.Finalized;
|
||||||
|
props.data.pendingOnForeignChain = true;
|
||||||
|
props.data.txHash = '0x123';
|
||||||
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Pending'));
|
||||||
|
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
expect.stringContaining(props.data.txHash)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Finalized', () => {
|
||||||
|
props.value = WithdrawalStatus.Finalized;
|
||||||
|
props.data.pendingOnForeignChain = false;
|
||||||
|
props.data.txHash = '0x123';
|
||||||
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Finalized'));
|
||||||
|
expect(screen.getByText('View on Etherscan')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
expect.stringContaining(props.data.txHash)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fallback', () => {
|
||||||
|
props.value = WithdrawalStatus.Rejected;
|
||||||
|
props.data.pendingOnForeignChain = false;
|
||||||
|
props.data.txHash = '0x123';
|
||||||
|
render(<StatusCell {...props} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Rejected'));
|
||||||
|
});
|
||||||
|
});
|
114
libs/withdraws/src/lib/withdrawals-table.tsx
Normal file
114
libs/withdraws/src/lib/withdrawals-table.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import type {
|
||||||
|
ICellRendererParams,
|
||||||
|
ValueFormatterParams,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import { AgGridColumn } from 'ag-grid-react';
|
||||||
|
import {
|
||||||
|
getDateTimeFormat,
|
||||||
|
t,
|
||||||
|
truncateByChars,
|
||||||
|
formatNumber,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { WithdrawalStatus } from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
EtherscanLink,
|
||||||
|
AgGridDynamic as AgGrid,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { TransactionDialog } from '@vegaprotocol/web3';
|
||||||
|
import { useCompleteWithdraw } from './use-complete-withdraw';
|
||||||
|
import type { Withdrawals_party_withdrawals } from './__generated__/Withdrawals';
|
||||||
|
|
||||||
|
export interface WithdrawalsTableProps {
|
||||||
|
withdrawals: Withdrawals_party_withdrawals[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithdrawalsTable = ({ withdrawals }: WithdrawalsTableProps) => {
|
||||||
|
const { transaction, submit } = useCompleteWithdraw();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AgGrid
|
||||||
|
rowData={withdrawals}
|
||||||
|
overlayNoRowsTemplate={t('No withdrawals')}
|
||||||
|
defaultColDef={{ flex: 1, resizable: true }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
components={{ StatusCell, RecipientCell }}
|
||||||
|
suppressCellFocus={true}
|
||||||
|
>
|
||||||
|
<AgGridColumn headerName="Asset" field="asset.symbol" />
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Amount"
|
||||||
|
field="amount"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||||
|
return formatNumber(value, data.asset.decimals);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Recipient"
|
||||||
|
field="details.receiverAddress"
|
||||||
|
cellRenderer="RecipientCell"
|
||||||
|
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||||
|
return truncateByChars(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Created at"
|
||||||
|
field="createdTimestamp"
|
||||||
|
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||||
|
return getDateTimeFormat().format(new Date(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Status"
|
||||||
|
field="status"
|
||||||
|
cellRenderer="StatusCell"
|
||||||
|
cellRendererParams={{ complete: submit }}
|
||||||
|
/>
|
||||||
|
</AgGrid>
|
||||||
|
<TransactionDialog name="withdraw" {...transaction} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StatusCellProps extends ICellRendererParams {
|
||||||
|
complete: (withdrawalId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusCell = ({ value, data, complete }: StatusCellProps) => {
|
||||||
|
if (data.pendingOnForeignChain) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between gap-8">
|
||||||
|
{t('Pending')}
|
||||||
|
{data.txHash && (
|
||||||
|
<EtherscanLink tx={data.txHash} text={t('View on Etherscan')} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === WithdrawalStatus.Finalized) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between gap-8">
|
||||||
|
{data.txHash ? (
|
||||||
|
<>
|
||||||
|
{t('Finalized')}
|
||||||
|
<EtherscanLink tx={data.txHash} text={t('View on Etherscan')} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{t('Open')}
|
||||||
|
<button className="underline" onClick={() => complete(data.id)}>
|
||||||
|
{t('Complete')}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecipientCell = ({ value, valueFormatted }: ICellRendererParams) => {
|
||||||
|
return <EtherscanLink address={value} text={valueFormatted} />;
|
||||||
|
};
|
1
libs/withdraws/src/setup-tests.ts
Normal file
1
libs/withdraws/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
25
libs/withdraws/tsconfig.json
Normal file
25
libs/withdraws/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
libs/withdraws/tsconfig.lib.json
Normal file
22
libs/withdraws/tsconfig.lib.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.test.jsx"
|
||||||
|
],
|
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
|
}
|
20
libs/withdraws/tsconfig.spec.json
Normal file
20
libs/withdraws/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"src/lib/withdraw-manager.foo.tsx"
|
||||||
|
]
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user