Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d1af843a | ||
|
|
d52d16ff10 |
@@ -16,6 +16,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# run copies of the current job in parallel
|
||||
containers: [1, 2]
|
||||
project: ${{ fromJSON(inputs.projects) }}
|
||||
runs-on: self-hosted-runner
|
||||
timeout-minutes: 30
|
||||
@@ -64,7 +66,7 @@ jobs:
|
||||
######
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --group 'UI - Chrome' --key ${{ secrets.CYPRESS_RECORD_KEY }} --parallel --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"tranche_end": "2023-04-06T00:00:00.000Z",
|
||||
"total_added": "14099",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "13755.4000425627232917",
|
||||
"locked_amount": "13982.940131421744864",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "30",
|
||||
@@ -2761,7 +2761,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "64879.699162507033901597",
|
||||
"locked_amount": "64998.4915697280591236161",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -2827,7 +2827,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1184.67738858363875",
|
||||
"locked_amount": "1191.549654049654",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"tranche_end": "2023-09-01T00:00:00.000Z",
|
||||
"total_added": "17500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "16952.60322312802",
|
||||
"locked_amount": "17000.1861916264105",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -3215,7 +3215,7 @@
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "3639.24714135",
|
||||
"locked_amount": "30506.46005985267",
|
||||
"locked_amount": "30610.11356660527875",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -3414,7 +3414,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "64820.504600498023491807",
|
||||
"locked_amount": "64939.188624594488977815",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -3480,7 +3480,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "30913.16868340943934",
|
||||
"locked_amount": "30998.97371892440273",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -3673,7 +3673,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2660.8837519025875",
|
||||
"locked_amount": "2667.737189244038",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -3884,7 +3884,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "6805.0783328659465926588",
|
||||
"locked_amount": "6921.4963045876377940008",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -3917,7 +3917,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "9301.36723326549382327399362",
|
||||
"locked_amount": "9460.49050188470449793150862",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -3963,7 +3963,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2862.778937219710450476",
|
||||
"locked_amount": "2911.7539674921900567086",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -3996,7 +3996,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "931.8067643157678690636",
|
||||
"locked_amount": "947.7476614270151353672",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -4029,7 +4029,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "4720.860935375",
|
||||
"locked_amount": "3483.3018002945028144642",
|
||||
"locked_amount": "3542.892434031412717479",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -4181,7 +4181,7 @@
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "5176.28289975",
|
||||
"locked_amount": "6867.411947513811",
|
||||
"locked_amount": "6929.60405156537625",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -4500,7 +4500,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "463693.4479242731446593956",
|
||||
"locked_amount": "466352.483444119808909542",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -36891,7 +36891,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "618385.665327542135993",
|
||||
"locked_amount": "737036.71327714871630535621",
|
||||
"locked_amount": "741122.72509331228813693143",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -38250,8 +38250,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "589039.38568474293710452",
|
||||
"locked_amount": "7913172.44864162203140200442918599770965126",
|
||||
"total_removed": "588645.45995589773910452",
|
||||
"locked_amount": "7927661.1841947058519192726441313236036767",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -38840,11 +38840,6 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xa7e962b98ed06faceaf876c50efca7276439f25eeb25ec574e88767fa0c5fb30"
|
||||
},
|
||||
{
|
||||
"amount": "393.925728845198",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x3ea8ed6b0441f5a71a645193ec134c51626bc8c9eb6442600da8482d41dd7c68"
|
||||
},
|
||||
{
|
||||
"amount": "535.4042378778335",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -40674,12 +40669,6 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xa7e962b98ed06faceaf876c50efca7276439f25eeb25ec574e88767fa0c5fb30"
|
||||
},
|
||||
{
|
||||
"amount": "393.925728845198",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x3ea8ed6b0441f5a71a645193ec134c51626bc8c9eb6442600da8482d41dd7c68"
|
||||
},
|
||||
{
|
||||
"amount": "535.4042378778335",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -41894,8 +41883,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "130355.869215303743",
|
||||
"remaining_tokens": "129643.018284696257"
|
||||
"withdrawn_tokens": "129961.943486458545",
|
||||
"remaining_tokens": "130036.944013541455"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -44168,8 +44157,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "5581251.054619088719392406",
|
||||
"locked_amount": "1583945.05221249870701141180921643",
|
||||
"total_removed": "5580710.615452109580396656",
|
||||
"locked_amount": "1597320.960363656005279201233314886",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -44433,11 +44422,6 @@
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x8636472fc307ddde44f9a3deca4a1f79fc49ae3741a3025c2b1dbc2cffc7e927"
|
||||
},
|
||||
{
|
||||
"amount": "540.43916697913899575",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xafba0370359a26f6fb9cceebc691c927bc820834384ffa51cd9565240e3a2a59"
|
||||
},
|
||||
{
|
||||
"amount": "800.792718381487871",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -47459,12 +47443,6 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0x8636472fc307ddde44f9a3deca4a1f79fc49ae3741a3025c2b1dbc2cffc7e927"
|
||||
},
|
||||
{
|
||||
"amount": "540.43916697913899575",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xafba0370359a26f6fb9cceebc691c927bc820834384ffa51cd9565240e3a2a59"
|
||||
},
|
||||
{
|
||||
"amount": "800.792718381487871",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -49981,8 +49959,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "320148.078288581844379",
|
||||
"remaining_tokens": "38975.391286418155621"
|
||||
"withdrawn_tokens": "319607.63912160270538325",
|
||||
"remaining_tokens": "39515.83045339729461675"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -51326,7 +51304,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "3077573.36197477479025324",
|
||||
"locked_amount": "308922.8662088976060115537634935795",
|
||||
"locked_amount": "314207.7684162386449535136951464068",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -53398,7 +53376,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "35108.9309805010685",
|
||||
"locked_amount": "116787.680300232033164283987519012",
|
||||
"locked_amount": "117435.132229142466206264400202944",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
|
||||
@@ -41,7 +41,6 @@ const completeWithdrawalBtn = 'complete-withdrawal';
|
||||
const submitTransferBtn = '[type="submit"]';
|
||||
const transferForm = 'transfer-form';
|
||||
const depositSubmit = 'deposit-submit';
|
||||
const approveSubmit = 'approve-submit';
|
||||
|
||||
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
|
||||
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
@@ -74,13 +73,13 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('approve-default').should(
|
||||
cy.getByTestId('approve-warning').should(
|
||||
'contain.text',
|
||||
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
|
||||
`Deposits of ${btcSymbol} not approved`
|
||||
);
|
||||
cy.getByTestId(approveSubmit).click();
|
||||
cy.getByTestId('approve-pending').should('exist');
|
||||
cy.getByTestId('approve-confirmed').should('exist');
|
||||
cy.getByTestId(depositSubmit).click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10');
|
||||
cy.getByTestId(depositSubmit).click();
|
||||
cy.getByTestId(toastContent, txTimeout).should(
|
||||
|
||||
@@ -31,13 +31,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
// once Ethereum wallet is connected and key selected the only field that will
|
||||
// error is the asset select
|
||||
cy.getByTestId(formFieldError).should('have.length', 1);
|
||||
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
|
||||
'have.length',
|
||||
1
|
||||
);
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
});
|
||||
|
||||
it('unable to select assets not enabled', () => {
|
||||
@@ -47,13 +41,12 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
|
||||
});
|
||||
|
||||
it('invalid public key when entering address manually', () => {
|
||||
cy.getByTestId('enter-pubkey-manually').click();
|
||||
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
|
||||
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
|
||||
'have.text',
|
||||
'Invalid Vega key'
|
||||
);
|
||||
it('invalid public key', () => {
|
||||
cy.get(toAddressField)
|
||||
.clear()
|
||||
.type('INVALID_DEPOSIT_TO_ADDRESS')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Invalid Vega key');
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
|
||||
cy.get('#ethereum-address').should('have.value', ethWalletAddress);
|
||||
cy.getByTestId('disconnect-ethereum-wallet')
|
||||
.should('have.text', 'Disconnect')
|
||||
.click();
|
||||
|
||||
@@ -177,14 +177,22 @@ export const useEthereumTransactionToasts = () => {
|
||||
store.remove,
|
||||
]);
|
||||
|
||||
const dismissTx = useEthTransactionStore((state) => state.dismiss);
|
||||
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
|
||||
state.dismiss,
|
||||
state.delete,
|
||||
]);
|
||||
|
||||
const onClose = useCallback(
|
||||
(tx: EthStoredTxState) => () => {
|
||||
dismissTx(tx.id);
|
||||
const safeToDelete = isFinal(tx);
|
||||
if (safeToDelete) {
|
||||
deleteTx(tx.id);
|
||||
} else {
|
||||
dismissTx(tx.id);
|
||||
}
|
||||
removeToast(`eth-${tx.id}`);
|
||||
},
|
||||
[dismissTx, removeToast]
|
||||
[deleteTx, dismissTx, removeToast]
|
||||
);
|
||||
|
||||
const fromEthTransaction = useCallback(
|
||||
|
||||
@@ -104,7 +104,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Total')}
|
||||
type="rightAligned"
|
||||
field="deposited"
|
||||
headerTooltip={t(
|
||||
'This is the total amount of collateral used plus the amount available in your general account.'
|
||||
@@ -122,7 +121,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Used')}
|
||||
type="rightAligned"
|
||||
field="used"
|
||||
headerTooltip={t(
|
||||
'This is the amount of collateral used from your general account.'
|
||||
@@ -141,7 +139,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
|
||||
<AgGridColumn
|
||||
headerName={t('Available')}
|
||||
field="available"
|
||||
type="rightAligned"
|
||||
headerTooltip={t(
|
||||
'This is the amount of collateral available in your general account.'
|
||||
)}
|
||||
|
||||
@@ -22,11 +22,7 @@ export const ExpirySelector = ({
|
||||
const dateFormatted = formatForInput(date);
|
||||
const minDate = formatForInput(date);
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('Expiry time/date')}
|
||||
labelFor="expiration"
|
||||
compact={true}
|
||||
>
|
||||
<FormGroup label={t('Expiry time/date')} labelFor="expiration">
|
||||
<Input
|
||||
data-testid="date-picker-field"
|
||||
id="expiration"
|
||||
|
||||
@@ -26,11 +26,7 @@ export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('Direction')}
|
||||
labelFor="order-side-toggle"
|
||||
compact={true}
|
||||
>
|
||||
<FormGroup label={t('Direction')} labelFor="order-side-toggle">
|
||||
<Toggle
|
||||
id="order-side-toggle"
|
||||
name="order-side"
|
||||
|
||||
@@ -119,11 +119,7 @@ export const TimeInForceSelector = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t('Time in force')}
|
||||
labelFor="select-time-in-force"
|
||||
compact={true}
|
||||
>
|
||||
<FormGroup label={t('Time in force')} labelFor="select-time-in-force">
|
||||
<Select
|
||||
id="select-time-in-force"
|
||||
value={value}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const TypeSelector = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup label={t('Order type')} labelFor="order-type" compact={true}>
|
||||
<FormGroup label={t('Order type')} labelFor="order-type">
|
||||
<Toggle
|
||||
id="order-type"
|
||||
name="order-type"
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
|
||||
interface ApproveNotificationProps {
|
||||
isActive: boolean;
|
||||
selectedAsset?: Asset;
|
||||
onApprove: () => void;
|
||||
approved: boolean;
|
||||
balances: DepositBalances | null;
|
||||
amount: string;
|
||||
approveTxId: number | null;
|
||||
}
|
||||
|
||||
export const ApproveNotification = ({
|
||||
isActive,
|
||||
selectedAsset,
|
||||
onApprove,
|
||||
amount,
|
||||
balances,
|
||||
approved,
|
||||
approveTxId,
|
||||
}: ApproveNotificationProps) => {
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === approveTxId);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selectedAsset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!balances) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const approvePrompt = (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-default"
|
||||
message={t(
|
||||
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
action: onApprove,
|
||||
dataTestId: 'approve-submit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const reApprovePrompt = (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="reapprove-default"
|
||||
message={t(
|
||||
`Approve again to deposit more than ${formatNumber(
|
||||
balances.allowance.toString()
|
||||
)}`
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
action: onApprove,
|
||||
dataTestId: 'reapprove-submit',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const approvalFeedback = (
|
||||
<ApprovalTxFeedback
|
||||
tx={tx}
|
||||
selectedAsset={selectedAsset}
|
||||
allowance={balances.allowance}
|
||||
/>
|
||||
);
|
||||
|
||||
// always show requested and pending states
|
||||
if (
|
||||
tx &&
|
||||
[EthTxStatus.Requested, EthTxStatus.Pending, EthTxStatus.Complete].includes(
|
||||
tx.status
|
||||
)
|
||||
) {
|
||||
return approvalFeedback;
|
||||
}
|
||||
|
||||
if (!approved) {
|
||||
return approvePrompt;
|
||||
}
|
||||
|
||||
if (new BigNumber(amount).isGreaterThan(balances.allowance)) {
|
||||
return reApprovePrompt;
|
||||
}
|
||||
|
||||
if (
|
||||
tx &&
|
||||
tx.status === EthTxStatus.Error &&
|
||||
// @ts-ignore tx.error not typed correctly
|
||||
tx.error.code === 'ACTION_REJECTED'
|
||||
) {
|
||||
return approvePrompt;
|
||||
}
|
||||
|
||||
return approvalFeedback;
|
||||
};
|
||||
|
||||
const ApprovalTxFeedback = ({
|
||||
tx,
|
||||
selectedAsset,
|
||||
allowance,
|
||||
}: {
|
||||
tx: EthStoredTxState | undefined;
|
||||
selectedAsset: Asset;
|
||||
allowance?: BigNumber;
|
||||
}) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
if (!tx) return null;
|
||||
|
||||
const txLink = tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="approve-error"
|
||||
message={
|
||||
<p>
|
||||
{t('Approval failed')} {txLink}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-requested"
|
||||
message={t(
|
||||
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Primary}
|
||||
testId="approve-pending"
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
|
||||
)}{' '}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Success}
|
||||
testId="approve-confirmed"
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`You can now make deposits in ${
|
||||
selectedAsset?.symbol
|
||||
}, up to a maximum of ${formatNumber(
|
||||
allowance?.toString() || 0
|
||||
)}`
|
||||
)}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -4,11 +4,18 @@ import { DepositManager } from './deposit-manager';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { enabledAssetsProvider } from '@vegaprotocol/assets';
|
||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const DepositContainer = ({ assetId }: { assetId?: string }) => {
|
||||
export const DepositContainer = ({
|
||||
assetId,
|
||||
setDialogStyleProps,
|
||||
}: {
|
||||
assetId?: string;
|
||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
||||
}) => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: enabledAssetsProvider,
|
||||
@@ -21,6 +28,7 @@ export const DepositContainer = ({ assetId }: { assetId?: string }) => {
|
||||
assetId={assetId}
|
||||
assets={data}
|
||||
isFaucetable={VEGA_ENV !== Networks.MAINNET}
|
||||
setDialogStyleProps={setDialogStyleProps}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { create } from 'zustand';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DepositContainer } from './deposit-container';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
@@ -22,6 +24,22 @@ export const useDepositDialog = create<State & Actions>((set) => ({
|
||||
close: () => set(() => ({ assetId: undefined, isOpen: false })),
|
||||
}));
|
||||
|
||||
export type DepositDialogStyleProps = {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
intent?: Intent;
|
||||
};
|
||||
|
||||
export type DepositDialogStylePropsSetter = (
|
||||
props?: DepositDialogStyleProps
|
||||
) => void;
|
||||
|
||||
const DEFAULT_STYLE: DepositDialogStyleProps = {
|
||||
title: t('Deposit'),
|
||||
intent: undefined,
|
||||
icon: undefined,
|
||||
};
|
||||
|
||||
export const DepositDialog = () => {
|
||||
const { assetId, isOpen, open, close } = useDepositDialog();
|
||||
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
|
||||
@@ -30,13 +48,25 @@ export const DepositDialog = () => {
|
||||
const connectWalletDialogIsOpen = useWeb3ConnectStore(
|
||||
(state) => state.isOpen
|
||||
);
|
||||
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);
|
||||
const setDialogStyleProps: DepositDialogStylePropsSetter =
|
||||
useCallback<DepositDialogStylePropsSetter>(
|
||||
(props) =>
|
||||
props
|
||||
? _setDialogStyleProps(props)
|
||||
: _setDialogStyleProps(DEFAULT_STYLE),
|
||||
[_setDialogStyleProps]
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
|
||||
onChange={(isOpen) => (isOpen ? open() : close())}
|
||||
title={t('Deposit')}
|
||||
{...dialogStyleProps}
|
||||
>
|
||||
<DepositContainer assetId={assetId} />
|
||||
<DepositContainer
|
||||
assetId={assetId}
|
||||
setDialogStyleProps={setDialogStyleProps}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/web3');
|
||||
@@ -39,34 +38,27 @@ function generateAsset(): AssetFieldsFragment {
|
||||
|
||||
let asset: AssetFieldsFragment;
|
||||
let props: DepositFormProps;
|
||||
let balances: DepositBalances;
|
||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const MOCK_VEGA_KEY =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
balances = {
|
||||
balance: new BigNumber(5),
|
||||
max: new BigNumber(20),
|
||||
allowance: new BigNumber(30),
|
||||
deposited: new BigNumber(10),
|
||||
};
|
||||
props = {
|
||||
assets: [asset],
|
||||
selectedAsset: undefined,
|
||||
onSelectAsset: jest.fn(),
|
||||
balances,
|
||||
balance: new BigNumber(5),
|
||||
submitApprove: jest.fn(),
|
||||
submitDeposit: jest.fn(),
|
||||
submitFaucet: jest.fn(),
|
||||
onDisconnect: jest.fn(),
|
||||
approveTxId: null,
|
||||
faucetTxId: null,
|
||||
requestFaucet: jest.fn(),
|
||||
max: new BigNumber(20),
|
||||
deposited: new BigNumber(10),
|
||||
allowance: new BigNumber(30),
|
||||
isFaucetable: true,
|
||||
};
|
||||
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null, pubKeys: [] });
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null });
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
@@ -79,8 +71,7 @@ describe('Deposit form', () => {
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
|
||||
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue(
|
||||
MOCK_ETH_ADDRESS
|
||||
);
|
||||
expect(screen.getByLabelText('Asset')).toHaveValue('');
|
||||
@@ -154,7 +145,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above lifetime deposit limit')
|
||||
await screen.findByText('Amount is above deposit limit')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -162,12 +153,9 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
balances={{
|
||||
...balances,
|
||||
balance: BigNumber(100),
|
||||
max: new BigNumber(100),
|
||||
deposited: new BigNumber(10),
|
||||
}}
|
||||
balance={new BigNumber(100)}
|
||||
max={new BigNumber(100)}
|
||||
deposited={new BigNumber(10)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -178,7 +166,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
await screen.findByText('Amount is above approved amount.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -226,17 +214,14 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
balances={{
|
||||
...balances,
|
||||
allowance: new BigNumber(0),
|
||||
}}
|
||||
allowance={new BigNumber(0)}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('approve-default')).toHaveTextContent(
|
||||
`Before you can make a deposit of your chosen asset, ${asset.symbol}, you need to approve its use in your Ethereum wallet`
|
||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
||||
`Deposits of ${asset.symbol} not approved`
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
@@ -268,12 +253,10 @@ describe('Deposit form', () => {
|
||||
render(
|
||||
<DepositForm
|
||||
{...props}
|
||||
balances={{
|
||||
allowance: new BigNumber(100),
|
||||
balance,
|
||||
max,
|
||||
deposited,
|
||||
}}
|
||||
allowance={new BigNumber(100)}
|
||||
balance={balance}
|
||||
max={max}
|
||||
deposited={deposited}
|
||||
selectedAsset={asset}
|
||||
/>
|
||||
);
|
||||
@@ -337,10 +320,10 @@ describe('Deposit form', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Connect' })
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
|
||||
MOCK_ETH_ADDRESS
|
||||
);
|
||||
const fromInput = screen.getByLabelText('From (Ethereum address)');
|
||||
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
|
||||
expect(fromInput).toBeDisabled();
|
||||
expect(fromInput).toHaveAttribute('readonly');
|
||||
});
|
||||
|
||||
it('prevents submission if you are on the wrong chain', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { AssetOption } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ethereumAddress,
|
||||
@@ -19,16 +19,13 @@ import {
|
||||
RichSelect,
|
||||
Notification,
|
||||
Intent,
|
||||
ButtonLink,
|
||||
Select,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import type { FieldError } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
@@ -37,9 +34,6 @@ import {
|
||||
useWeb3ConnectStore,
|
||||
getChainName,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
import { FaucetNotification } from './faucet-notification';
|
||||
import { ApproveNotification } from './approve-notification';
|
||||
|
||||
interface FormFields {
|
||||
asset: string;
|
||||
@@ -51,38 +45,38 @@ interface FormFields {
|
||||
export interface DepositFormProps {
|
||||
assets: Asset[];
|
||||
selectedAsset?: Asset;
|
||||
balances: DepositBalances | null;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
onDisconnect: () => void;
|
||||
balance: BigNumber | undefined;
|
||||
submitApprove: () => void;
|
||||
approveTxId: number | null;
|
||||
submitFaucet: () => void;
|
||||
faucetTxId: number | null;
|
||||
submitDeposit: (args: {
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}) => void;
|
||||
requestFaucet: () => void;
|
||||
max: BigNumber | undefined;
|
||||
deposited: BigNumber | undefined;
|
||||
allowance: BigNumber | undefined;
|
||||
isFaucetable?: boolean;
|
||||
}
|
||||
|
||||
export const DepositForm = ({
|
||||
assets,
|
||||
selectedAsset,
|
||||
balances,
|
||||
onSelectAsset,
|
||||
onDisconnect,
|
||||
balance,
|
||||
max,
|
||||
deposited,
|
||||
submitApprove,
|
||||
submitDeposit,
|
||||
submitFaucet,
|
||||
faucetTxId,
|
||||
approveTxId,
|
||||
requestFaucet,
|
||||
allowance,
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -97,21 +91,43 @@ export const DepositForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||
throw new Error('Invalid asset');
|
||||
}
|
||||
if (!approved) throw new Error('Deposits not approved');
|
||||
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
if (approved) {
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
} else {
|
||||
submitApprove();
|
||||
}
|
||||
};
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : new BigNumber(0);
|
||||
const maxAvailable = balance ? balance : new BigNumber(0);
|
||||
|
||||
// limits.max is a lifetime deposit limit, so the actual max value for form
|
||||
// input is the max minus whats already been deposited
|
||||
let maxLimit = new BigNumber(Infinity);
|
||||
|
||||
// A max limit of zero indicates that there is no limit
|
||||
if (max && deposited && max.isGreaterThan(0)) {
|
||||
maxLimit = max.minus(deposited);
|
||||
}
|
||||
|
||||
return {
|
||||
approved: maxApproved,
|
||||
available: maxAvailable,
|
||||
limit: maxLimit,
|
||||
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
|
||||
};
|
||||
}, [max, deposited, allowance, balance]);
|
||||
|
||||
const min = useMemo(() => {
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const minViableAmount = selectedAsset
|
||||
@@ -121,15 +137,8 @@ export const DepositForm = ({
|
||||
return minViableAmount;
|
||||
}, [selectedAsset]);
|
||||
|
||||
const pubKeys = useMemo(() => {
|
||||
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
|
||||
}, [_pubKeys]);
|
||||
|
||||
const approved = balances
|
||||
? balances.allowance.isGreaterThan(0)
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
||||
const formState = getFormState(selectedAsset, isActive, approved);
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -157,23 +166,32 @@ export const DepositForm = ({
|
||||
render={() => {
|
||||
if (isActive && account) {
|
||||
return (
|
||||
<div className="text-sm" aria-describedby="ethereum-address">
|
||||
<p className="mb-1" data-testid="ethereum-address">
|
||||
{account}
|
||||
</p>
|
||||
<>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
value={account}
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
{...register('from', {
|
||||
validate: {
|
||||
required,
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<DisconnectEthereumButton
|
||||
onDisconnect={() => {
|
||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||
onDisconnect();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={openDialog}
|
||||
variant="primary"
|
||||
fill={true}
|
||||
type="button"
|
||||
data-testid="connect-eth-wallet-btn"
|
||||
>
|
||||
@@ -220,7 +238,7 @@ export const DepositForm = ({
|
||||
</InputError>
|
||||
)}
|
||||
{isFaucetable && selectedAsset && (
|
||||
<UseButton onClick={submitFaucet}>
|
||||
<UseButton onClick={requestFaucet}>
|
||||
{t(`Get ${selectedAsset.symbol}`)}
|
||||
</UseButton>
|
||||
)}
|
||||
@@ -237,55 +255,39 @@ export const DepositForm = ({
|
||||
</button>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FaucetNotification
|
||||
isActive={isActive}
|
||||
selectedAsset={selectedAsset}
|
||||
faucetTxId={faucetTxId}
|
||||
/>
|
||||
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('to', '')}
|
||||
select={
|
||||
<Select {...register('to')} id="to" defaultValue="">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
input={
|
||||
<Input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to"
|
||||
type="text"
|
||||
{...register('to', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
<Input
|
||||
{...register('to', { validate: { required, vegaPublicKey } })}
|
||||
id="to"
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger" forInput="to">
|
||||
{errors.to.message}
|
||||
</InputError>
|
||||
)}
|
||||
{pubKey && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('to', pubKey);
|
||||
clearErrors('to');
|
||||
}}
|
||||
>
|
||||
{t('Use connected')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && balances && (
|
||||
{selectedAsset && max && deposited && (
|
||||
<div className="mb-6">
|
||||
<DepositLimits {...balances} asset={selectedAsset} />
|
||||
<DepositLimits
|
||||
max={max}
|
||||
deposited={deposited}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
allowance={allowance}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{approved && (
|
||||
{formState === 'deposit' && (
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -297,52 +299,38 @@ export const DepositForm = ({
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
approved: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(balances?.allowance || 0)) {
|
||||
if (value.isGreaterThan(maxAmount.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
limit: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (!balances) {
|
||||
return t('Could not verify balances of account'); // this should never happen
|
||||
}
|
||||
|
||||
let lifetimeLimit = new BigNumber(Infinity);
|
||||
if (balances.max.isGreaterThan(0)) {
|
||||
lifetimeLimit = balances.max.minus(balances.deposited);
|
||||
}
|
||||
|
||||
if (value.isGreaterThan(lifetimeLimit)) {
|
||||
return t('Amount is above lifetime deposit limit');
|
||||
if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above deposit limit');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
balance: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(balances?.balance || 0)) {
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
return maxSafe(balances?.balance || new BigNumber(0))(v);
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
||||
)}
|
||||
{selectedAsset && balances && (
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue(
|
||||
'amount',
|
||||
balances.balance.toFixed(selectedAsset.decimals)
|
||||
);
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
@@ -351,31 +339,59 @@ export const DepositForm = ({
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<ApproveNotification
|
||||
isActive={isActive}
|
||||
approveTxId={approveTxId}
|
||||
selectedAsset={selectedAsset}
|
||||
onApprove={submitApprove}
|
||||
balances={balances}
|
||||
approved={approved}
|
||||
amount={amount}
|
||||
/>
|
||||
<FormButton approved={approved} selectedAsset={selectedAsset} />
|
||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const AmountError = ({
|
||||
error,
|
||||
submitApprove,
|
||||
}: {
|
||||
error: FieldError;
|
||||
submitApprove: () => void;
|
||||
}) => {
|
||||
if (error.type === 'approved') {
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}.
|
||||
<button onClick={submitApprove} className="underline ml-2">
|
||||
{t('Update approve amount')}
|
||||
</button>
|
||||
</InputError>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}
|
||||
</InputError>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormButtonProps {
|
||||
approved: boolean;
|
||||
selectedAsset: AssetFieldsFragment | undefined;
|
||||
selectedAsset?: Asset;
|
||||
formState: ReturnType<typeof getFormState>;
|
||||
}
|
||||
|
||||
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const submitText =
|
||||
formState === 'approve'
|
||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
||||
: t('Deposit');
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
return (
|
||||
<>
|
||||
{formState === 'approve' && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-warning"
|
||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invalidChain && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
@@ -392,9 +408,9 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
data-testid="deposit-submit"
|
||||
variant={isActive ? 'primary' : 'default'}
|
||||
fill={true}
|
||||
disabled={invalidChain || (selectedAsset && !approved)}
|
||||
disabled={invalidChain}
|
||||
>
|
||||
{t('Deposit')}
|
||||
{submitText}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -421,7 +437,7 @@ const DisconnectEthereumButton = ({
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
|
||||
return (
|
||||
<ButtonLink
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
connector.deactivate();
|
||||
removeEagerConnector();
|
||||
@@ -430,46 +446,17 @@ const DisconnectEthereumButton = ({
|
||||
data-testid="disconnect-ethereum-wallet"
|
||||
>
|
||||
{t('Disconnect')}
|
||||
</ButtonLink>
|
||||
</UseButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddressInputProps {
|
||||
pubKeys: string[] | null;
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
pubKeys,
|
||||
select,
|
||||
input,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
{pubKeys && pubKeys.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="ml-auto text-sm absolute top-0 right-0 underline"
|
||||
data-testid="enter-pubkey-manually"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const getFormState = (
|
||||
selectedAsset: Asset | undefined,
|
||||
isActive: boolean,
|
||||
approved: boolean
|
||||
) => {
|
||||
if (!selectedAsset) return 'deposit';
|
||||
if (!isActive) return 'deposit';
|
||||
if (approved) return 'deposit';
|
||||
return 'approve';
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DepositLimits = ({
|
||||
},
|
||||
{
|
||||
key: 'MAX_LIMIT',
|
||||
label: t('Lifetime deposit allowance'),
|
||||
label: t('Maximum total deposit amount'),
|
||||
rawValue: max,
|
||||
value: <CompactNumber number={max} decimals={asset.decimals} />,
|
||||
},
|
||||
|
||||
@@ -5,26 +5,36 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDepositBalances } from './use-deposit-balances';
|
||||
import { useDepositDialog } from './deposit-dialog';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
||||
import pick from 'lodash/pick';
|
||||
import type { EthTransaction } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthTransactionStore,
|
||||
useBridgeContract,
|
||||
useEthereumConfig,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface DepositManagerProps {
|
||||
assetId?: string;
|
||||
assets: Asset[];
|
||||
isFaucetable: boolean;
|
||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
||||
}
|
||||
|
||||
const getProps = (txContent?: EthTransaction['TxContent']) =>
|
||||
txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined;
|
||||
|
||||
export const DepositManager = ({
|
||||
assetId: initialAssetId,
|
||||
assets,
|
||||
isFaucetable,
|
||||
setDialogStyleProps,
|
||||
}: DepositManagerProps) => {
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const { config } = useEthereumConfig();
|
||||
@@ -33,16 +43,26 @@ export const DepositManager = ({
|
||||
const bridgeContract = useBridgeContract();
|
||||
const closeDepositDialog = useDepositDialog((state) => state.close);
|
||||
|
||||
const { getBalances, reset, balances } = useDepositBalances(
|
||||
const { balance, allowance, deposited, max, refresh } = useDepositBalances(
|
||||
asset,
|
||||
isFaucetable
|
||||
);
|
||||
|
||||
// Set up approve transaction
|
||||
const approve = useSubmitApproval(asset, getBalances);
|
||||
const approve = useSubmitApproval(asset);
|
||||
|
||||
// Set up faucet transaction
|
||||
const faucet = useSubmitFaucet(asset, getBalances);
|
||||
const faucet = useSubmitFaucet(asset);
|
||||
|
||||
const transactionInProgress = [approve.TxContent, faucet.TxContent].filter(
|
||||
(t) => t.status !== EthTxStatus.Default
|
||||
)[0];
|
||||
|
||||
useEffect(() => {
|
||||
setDialogStyleProps?.(getProps(transactionInProgress));
|
||||
}, [setDialogStyleProps, transactionInProgress]);
|
||||
|
||||
const returnLabel = t('Return to deposit');
|
||||
|
||||
const submitDeposit = (
|
||||
args: Parameters<DepositFormProps['submitDeposit']>['0']
|
||||
@@ -66,24 +86,31 @@ export const DepositManager = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<DepositForm
|
||||
selectedAsset={asset}
|
||||
onDisconnect={reset}
|
||||
onSelectAsset={(id) => {
|
||||
setAssetId(id);
|
||||
// When we change asset, also clear the tracked faucet/approve transactions so
|
||||
// we dont render stale UI
|
||||
approve.reset();
|
||||
faucet.reset();
|
||||
}}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={approve.perform}
|
||||
submitDeposit={submitDeposit}
|
||||
submitFaucet={faucet.perform}
|
||||
faucetTxId={faucet.id}
|
||||
approveTxId={approve.id}
|
||||
balances={balances}
|
||||
isFaucetable={isFaucetable}
|
||||
/>
|
||||
<>
|
||||
{!transactionInProgress && (
|
||||
<DepositForm
|
||||
balance={balance}
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={setAssetId}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={async () => {
|
||||
await approve.perform();
|
||||
refresh();
|
||||
}}
|
||||
submitDeposit={submitDeposit}
|
||||
requestFaucet={async () => {
|
||||
await faucet.perform();
|
||||
refresh();
|
||||
}}
|
||||
deposited={deposited}
|
||||
max={max}
|
||||
allowance={allowance}
|
||||
isFaucetable={isFaucetable}
|
||||
/>
|
||||
)}
|
||||
|
||||
<approve.TxContent.Content returnLabel={returnLabel} />
|
||||
<faucet.TxContent.Content returnLabel={returnLabel} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
|
||||
interface FaucetNotificationProps {
|
||||
isActive: boolean;
|
||||
selectedAsset?: Asset;
|
||||
faucetTxId: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a notification for the faucet transaction
|
||||
*/
|
||||
export const FaucetNotification = ({
|
||||
isActive,
|
||||
selectedAsset,
|
||||
faucetTxId,
|
||||
}: FaucetNotificationProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||
});
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!selectedAsset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="faucet-error"
|
||||
// @ts-ignore tx.error not typed correctly
|
||||
message={t(`Faucet failed: ${tx.error?.reason}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="faucet-requested"
|
||||
message={t(
|
||||
`Go to your Ethereum wallet and approve the faucet transaction for ${selectedAsset?.symbol}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Primary}
|
||||
testId="faucet-pending"
|
||||
message={
|
||||
<p>
|
||||
{t('Faucet pending...')}{' '}
|
||||
{tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Success}
|
||||
testId="faucet-confirmed"
|
||||
message={
|
||||
<p>
|
||||
{t('Faucet successful')}{' '}
|
||||
{tx.txHash && (
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -7,17 +7,21 @@ import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
import { useGetDepositMaximum } from './use-get-deposit-maximum';
|
||||
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import { usePrevious } from '@vegaprotocol/react-helpers';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
|
||||
export interface DepositBalances {
|
||||
balance: BigNumber; // amount in Ethereum wallet
|
||||
allowance: BigNumber; // amount approved
|
||||
deposited: BigNumber; // total amounted deposited over lifetime
|
||||
max: BigNumber; // life time deposit cap
|
||||
}
|
||||
type DepositBalances = {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
deposited: BigNumber;
|
||||
max: BigNumber;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
const initialState: DepositBalances = {
|
||||
type DepositBalancesState = Omit<DepositBalances, 'refresh'>;
|
||||
|
||||
const initialState: DepositBalancesState = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
deposited: new BigNumber(0),
|
||||
@@ -31,7 +35,7 @@ const initialState: DepositBalances = {
|
||||
export const useDepositBalances = (
|
||||
asset: Asset | undefined,
|
||||
isFaucetable: boolean
|
||||
) => {
|
||||
): DepositBalances => {
|
||||
const tokenContract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
isFaucetable
|
||||
@@ -41,14 +45,21 @@ export const useDepositBalances = (
|
||||
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
||||
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
|
||||
const getDepositedAmount = useGetDepositedAmount(asset);
|
||||
const [state, setState] = useState<DepositBalances | null>(null);
|
||||
const prevAsset = usePrevious(asset);
|
||||
const [state, setState] = useState<DepositBalancesState>(initialState);
|
||||
|
||||
useEffect(() => {
|
||||
if (asset?.id !== prevAsset?.id) {
|
||||
// reset values to initial state when asset changes
|
||||
setState(initialState);
|
||||
}
|
||||
}, [asset?.id, prevAsset?.id]);
|
||||
|
||||
const { accountBalance } = useAccountBalance(asset?.id);
|
||||
|
||||
const getBalances = useCallback(async () => {
|
||||
if (!asset) return;
|
||||
try {
|
||||
setState(null);
|
||||
const [max, deposited, balance, allowance] = await Promise.all([
|
||||
getDepositMaximum(),
|
||||
getDepositedAmount(),
|
||||
@@ -64,17 +75,12 @@ export const useDepositBalances = (
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
setState(null);
|
||||
}
|
||||
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setState(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getBalances();
|
||||
}, [asset, getBalances, accountBalance]);
|
||||
|
||||
return { balances: state, getBalances, reset };
|
||||
return { ...state, refresh: getBalances };
|
||||
};
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { Token } from '@vegaprotocol/smart-contracts';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthereumConfig,
|
||||
useEthTransactionStore,
|
||||
useEthereumTransaction,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useSubmitApproval = (
|
||||
asset: Asset | undefined,
|
||||
getBalances: () => void
|
||||
) => {
|
||||
const [id, setId] = useState<number | null>(null);
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === id);
|
||||
});
|
||||
export const useSubmitApproval = (asset?: Asset) => {
|
||||
const { config } = useEthereumConfig();
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// When tx is confirmed refresh balances
|
||||
useEffect(() => {
|
||||
if (tx?.status === EthTxStatus.Confirmed) {
|
||||
getBalances();
|
||||
}
|
||||
}, [tx?.status, getBalances]);
|
||||
|
||||
const transaction = useEthereumTransaction<Token, 'approve'>(
|
||||
contract,
|
||||
'approve'
|
||||
);
|
||||
return {
|
||||
id,
|
||||
reset: () => {
|
||||
setId(null);
|
||||
},
|
||||
perform: () => {
|
||||
...transaction,
|
||||
perform: async () => {
|
||||
if (!asset || !config) return;
|
||||
const amount = removeDecimal('1000000', asset.decimals);
|
||||
const id = createEthTransaction(contract, 'approve', [
|
||||
config?.collateral_bridge_contract.address,
|
||||
amount,
|
||||
]);
|
||||
setId(id);
|
||||
try {
|
||||
const amount = removeDecimal('1000000', asset.decimals);
|
||||
await transaction.perform(
|
||||
config.collateral_bridge_contract.address,
|
||||
amount
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,41 +1,26 @@
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthTransactionStore,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useSubmitFaucet = (
|
||||
asset: Asset | undefined,
|
||||
getBalances: () => void
|
||||
) => {
|
||||
const [id, setId] = useState<number | null>(null);
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === id);
|
||||
});
|
||||
export const useSubmitFaucet = (asset?: Asset) => {
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// When tx is confirmed refresh balances
|
||||
useEffect(() => {
|
||||
if (tx?.status === EthTxStatus.Confirmed) {
|
||||
getBalances();
|
||||
}
|
||||
}, [tx?.status, getBalances]);
|
||||
|
||||
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
|
||||
contract,
|
||||
'faucet'
|
||||
);
|
||||
return {
|
||||
id,
|
||||
reset: () => {
|
||||
setId(null);
|
||||
},
|
||||
perform: () => {
|
||||
const id = createEthTransaction(contract, 'faucet', []);
|
||||
setId(id);
|
||||
...transaction,
|
||||
perform: async () => {
|
||||
try {
|
||||
await transaction.perform();
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
|
||||
import { FillsTable, getFeesBreakdown } from './fills-table';
|
||||
import { FillsTable } from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
|
||||
describe('FillsTable', () => {
|
||||
@@ -75,7 +75,7 @@ describe('FillsTable', () => {
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'2.00 BTC',
|
||||
'0.06 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
@@ -176,36 +176,4 @@ describe('FillsTable', () => {
|
||||
await screen.findByTestId('fee-breakdown-tooltip')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('TAKER', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('MAKER', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,10 +27,6 @@ import type {
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import type { FillFieldsFragment } from './__generated__/Fills';
|
||||
|
||||
const TAKER = 'TAKER';
|
||||
const MAKER = 'MAKER';
|
||||
|
||||
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
partyId: string;
|
||||
@@ -232,132 +228,66 @@ const formatFee = (partyId: string) => {
|
||||
return '-';
|
||||
}
|
||||
const asset = value.settlementAsset;
|
||||
const { fees: feesObj, role } = getRoleAndFees({ data, partyId });
|
||||
if (!feesObj) return '-';
|
||||
let feesObj;
|
||||
if (data?.buyer.id === partyId) {
|
||||
feesObj = data?.buyerFee;
|
||||
} else if (data?.seller.id === partyId) {
|
||||
feesObj = data?.sellerFee;
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const { totalFee } = getFeesBreakdown(role, feesObj);
|
||||
const totalFees = addDecimalsFormatNumber(totalFee, asset.decimals);
|
||||
const fee = new BigNumber(feesObj.makerFee)
|
||||
.plus(feesObj.infrastructureFee)
|
||||
.plus(feesObj.liquidityFee);
|
||||
const totalFees = addDecimalsFormatNumber(fee.toString(), asset.decimals);
|
||||
return `${totalFees} ${asset.symbol}`;
|
||||
};
|
||||
};
|
||||
|
||||
export const getRoleAndFees = ({
|
||||
data,
|
||||
partyId,
|
||||
}: {
|
||||
data: Pick<
|
||||
FillFieldsFragment,
|
||||
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
|
||||
>;
|
||||
partyId?: string;
|
||||
}) => {
|
||||
let role;
|
||||
let feesObj;
|
||||
if (data?.buyer.id === partyId) {
|
||||
role = data.aggressor === Schema.Side.SIDE_BUY ? TAKER : MAKER;
|
||||
feesObj = role === TAKER ? data?.buyerFee : data.sellerFee;
|
||||
} else if (data?.seller.id === partyId) {
|
||||
role = data.aggressor === Schema.Side.SIDE_SELL ? TAKER : MAKER;
|
||||
feesObj = role === TAKER ? data?.sellerFee : data.buyerFee;
|
||||
} else {
|
||||
return { role: '-', feesObj: '-' };
|
||||
}
|
||||
return { role, fees: feesObj };
|
||||
};
|
||||
|
||||
const FeesBreakdownTooltip = ({
|
||||
data,
|
||||
value,
|
||||
valueFormatted,
|
||||
partyId,
|
||||
}: ITooltipParams & { partyId?: string }) => {
|
||||
if (!value?.settlementAsset || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asset = value.settlementAsset;
|
||||
|
||||
const { role, fees: feesObj } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!feesObj) return null;
|
||||
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
|
||||
getFeesBreakdown(role, feesObj);
|
||||
let feesObj;
|
||||
if (data?.buyer.id === partyId) {
|
||||
feesObj = data?.buyerFee;
|
||||
} else if (data?.seller.id === partyId) {
|
||||
feesObj = data?.sellerFee;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-breakdown-tooltip"
|
||||
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
|
||||
>
|
||||
{role === MAKER && (
|
||||
<>
|
||||
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is in monitoring auction the maker will pay half of the infrastructure and liquidity fees.'
|
||||
)}
|
||||
</p>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is active the maker will pay zero infrastructure and liquidity fees.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{role === TAKER && (
|
||||
<>
|
||||
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is in monitoring auction the taker will pay half of the infrastructure and liquidity fees.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
<dl className="grid grid-cols-3 gap-x-1">
|
||||
<dt className="col-span-1">{t('Infrastructure fee')}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
{addDecimalsFormatNumber(infrastructureFee, asset.decimals)}{' '}
|
||||
<dd className="text-right col-span-2">
|
||||
{addDecimalsFormatNumber(feesObj.infrastructureFee, asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</dd>
|
||||
<dt className="col-span-1">{t('Liquidity fee')}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
{addDecimalsFormatNumber(liquidityFee, asset.decimals)} {asset.symbol}
|
||||
<dd className="text-right col-span-2">
|
||||
{addDecimalsFormatNumber(feesObj.liquidityFee, asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</dd>
|
||||
<dt className="col-span-1">{t('Maker fee')}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
{addDecimalsFormatNumber(makerFee, asset.decimals)} {asset.symbol}
|
||||
<dd className="text-right col-span-2">
|
||||
{addDecimalsFormatNumber(feesObj.makerFee, asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</dd>
|
||||
<dt className="col-span-1">{t('Total fees')}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
{addDecimalsFormatNumber(totalFee, asset.decimals)} {asset.symbol}
|
||||
</dd>
|
||||
<dd className="text-right col-span-2">{valueFormatted}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: string,
|
||||
feesObj: {
|
||||
__typename?: 'TradeFee' | undefined;
|
||||
makerFee: string;
|
||||
infrastructureFee: string;
|
||||
liquidityFee: string;
|
||||
}
|
||||
) => {
|
||||
const makerFee =
|
||||
role === MAKER
|
||||
? new BigNumber(feesObj.makerFee).times(-1).toString()
|
||||
: feesObj.makerFee;
|
||||
const infrastructureFee = feesObj.infrastructureFee;
|
||||
const liquidityFee = feesObj.liquidityFee;
|
||||
|
||||
const totalFee = new BigNumber(infrastructureFee)
|
||||
.plus(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.toString();
|
||||
return {
|
||||
infrastructureFee,
|
||||
liquidityFee,
|
||||
makerFee,
|
||||
totalFee,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -88,9 +88,9 @@ const getRowsToRender = (
|
||||
return selectedRows;
|
||||
};
|
||||
|
||||
// 17px of row height plus 4px gap
|
||||
export const gridGap = 4;
|
||||
export const rowHeight = 21;
|
||||
// 17px of row height plus 5px gap
|
||||
export const gridGap = 5;
|
||||
export const rowHeight = 22;
|
||||
// top padding to make space for header
|
||||
const headerPadding = 30;
|
||||
// bottom padding to make space for footer
|
||||
@@ -393,8 +393,7 @@ export const Orderbook = ({
|
||||
numberOfRows * rowHeight +
|
||||
headerPadding +
|
||||
footerPadding +
|
||||
-viewportHeight -
|
||||
gridGap
|
||||
-viewportHeight
|
||||
)
|
||||
);
|
||||
if (scrollTopRef.current !== priceCenterScrollOffset) {
|
||||
|
||||
@@ -4,8 +4,6 @@ fragment PositionFields on Position {
|
||||
unrealisedPNL
|
||||
averageEntryPrice
|
||||
updatedAt
|
||||
positionStatus
|
||||
lossSocializationAmount
|
||||
market {
|
||||
id
|
||||
}
|
||||
@@ -32,8 +30,6 @@ subscription PositionsSubscription($partyId: ID!) {
|
||||
averageEntryPrice
|
||||
updatedAt
|
||||
marketId
|
||||
lossSocializationAmount
|
||||
positionStatus
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-7
@@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } };
|
||||
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type PositionsQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
|
||||
|
||||
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> };
|
||||
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string }> };
|
||||
|
||||
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
@@ -42,8 +42,6 @@ export const PositionFieldsFragmentDoc = gql`
|
||||
unrealisedPNL
|
||||
averageEntryPrice
|
||||
updatedAt
|
||||
positionStatus
|
||||
lossSocializationAmount
|
||||
market {
|
||||
id
|
||||
}
|
||||
@@ -114,8 +112,6 @@ export const PositionsSubscriptionDocument = gql`
|
||||
averageEntryPrice
|
||||
updatedAt
|
||||
marketId
|
||||
lossSocializationAmount
|
||||
positionStatus
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
MarginFieldsFragment,
|
||||
} from './__generated__/Positions';
|
||||
import { getMetrics, rejoinPositionData } from './positions-data-providers';
|
||||
import { PositionStatus } from '@vegaprotocol/types';
|
||||
|
||||
const accounts = [
|
||||
{
|
||||
@@ -79,8 +78,6 @@ const positions: PositionFieldsFragment[] = [
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
lossSocializationAmount: '0',
|
||||
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
__typename: 'Position',
|
||||
@@ -93,8 +90,6 @@ const positions: PositionFieldsFragment[] = [
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
lossSocializationAmount: '100',
|
||||
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -230,10 +225,6 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[0].totalBalance).toEqual('926178496');
|
||||
expect(metrics[0].unrealisedPNL).toEqual('43804770');
|
||||
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
|
||||
expect(metrics[0].lossSocializationAmount).toEqual(
|
||||
positions[0].lossSocializationAmount
|
||||
);
|
||||
expect(metrics[0].status).toEqual(positions[0].positionStatus);
|
||||
|
||||
expect(metrics[1].assetSymbol).toEqual('tDAI');
|
||||
expect(metrics[1].averageEntryPrice).toEqual('840158');
|
||||
@@ -257,9 +248,5 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
expect(metrics[1].totalBalance).toEqual('896098819');
|
||||
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
|
||||
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
|
||||
expect(metrics[1].lossSocializationAmount).toEqual(
|
||||
positions[1].lossSocializationAmount
|
||||
);
|
||||
expect(metrics[1].status).toEqual(positions[1].positionStatus);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
PositionsSubscriptionDocument,
|
||||
} from './__generated__/Positions';
|
||||
import { marginsDataProvider } from './margin-data-provider';
|
||||
import type { PositionStatus } from '@vegaprotocol/types';
|
||||
|
||||
type PositionMarginLevel = Pick<
|
||||
MarginFieldsFragment,
|
||||
@@ -39,8 +38,6 @@ interface PositionRejoined {
|
||||
updatedAt?: string | null;
|
||||
market: MarketMaybeWithData | null;
|
||||
margins: PositionMarginLevel | null;
|
||||
lossSocializationAmount: string | null;
|
||||
status: PositionStatus;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
@@ -65,8 +62,6 @@ export interface Position {
|
||||
unrealisedPNL: string;
|
||||
searchPrice: string | undefined;
|
||||
updatedAt: string | null;
|
||||
lossSocializationAmount: string;
|
||||
status: PositionStatus;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
@@ -183,8 +178,6 @@ export const getMetrics = (
|
||||
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||
: undefined,
|
||||
updatedAt: position.updatedAt || null,
|
||||
lossSocializationAmount: position.lossSocializationAmount || '0',
|
||||
status: position.status,
|
||||
});
|
||||
});
|
||||
return metrics;
|
||||
@@ -208,8 +201,6 @@ export const update = (
|
||||
openVolume: delta.openVolume,
|
||||
averageEntryPrice: delta.averageEntryPrice,
|
||||
updatedAt: delta.updatedAt,
|
||||
lossSocializationAmount: delta.lossSocializationAmount,
|
||||
positionStatus: delta.positionStatus,
|
||||
};
|
||||
} else {
|
||||
draft.unshift({
|
||||
@@ -276,8 +267,6 @@ export const rejoinPositionData = (
|
||||
market:
|
||||
marketsData?.find((market) => market.id === node.market.id) || null,
|
||||
margins: upgradeMarginsConnection(node.market.id, margins),
|
||||
lossSocializationAmount: node.lossSocializationAmount,
|
||||
status: node.positionStatus,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { RenderResult } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import PositionsTable from './positions-table';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
|
||||
const singleRow: Position = {
|
||||
marketName: 'ETH/BTC (31 july 2022)',
|
||||
@@ -29,8 +26,6 @@ const singleRow: Position = {
|
||||
searchPrice: '0',
|
||||
updatedAt: '2022-07-27T15:02:58.400Z',
|
||||
marginAccountBalance: '12345600',
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
lossSocializationAmount: '0',
|
||||
};
|
||||
|
||||
const singleRowData = [singleRow];
|
||||
@@ -224,97 +219,3 @@ it('do not display close button if openVolume is zero', async () => {
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[12].textContent).toEqual('');
|
||||
});
|
||||
|
||||
describe('PNLCell', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no loss socialisation has occurred', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocialisationAmount: '0',
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value with warning tooltip if loss socialisation occurred', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocializationAmount: '500',
|
||||
decimals: 2,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText('Lifetime loss socialisation deductions: 5.00')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenVolumeCell', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value if no status is normal', () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if not normal', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CellRendererSelectorResult } from 'ag-grid-community';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
@@ -15,15 +15,6 @@ import {
|
||||
signedNumberCssClass,
|
||||
signedNumberCssClassRules,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
ButtonLink,
|
||||
Tooltip,
|
||||
TooltipCellComponent,
|
||||
Link,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
ProgressBarCell,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
volumePrefix,
|
||||
toBigNum,
|
||||
@@ -36,9 +27,13 @@ import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
ButtonLink,
|
||||
Link,
|
||||
ProgressBarCell,
|
||||
TooltipCellComponent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { getRowId } from './use-positions-data';
|
||||
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
interface Props extends TypedDataAgGrid<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
@@ -172,7 +167,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
)
|
||||
);
|
||||
}}
|
||||
cellRenderer={OpenVolumeCell}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
@@ -317,6 +311,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
||||
| string
|
||||
| undefined => {
|
||||
@@ -334,7 +329,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="font-mono text-right"
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
@@ -353,14 +348,13 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
headerTooltip={t(
|
||||
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
|
||||
)}
|
||||
cellRenderer={PNLCell}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
cellClass="font-mono text-right"
|
||||
cellClass="text-right font-mono"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
@@ -379,7 +373,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
headerTooltip={t(
|
||||
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
|
||||
)}
|
||||
cellRenderer={PNLCell}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
@@ -416,106 +409,3 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
);
|
||||
|
||||
export default PositionsTable;
|
||||
|
||||
export const PNLCell = ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
if (!data) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
const losses = parseInt(data?.lossSocializationAmount ?? '0');
|
||||
if (losses <= 0) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{valueFormatted}</>;
|
||||
}
|
||||
|
||||
const lossesFormatted = addDecimalsFormatNumber(
|
||||
data.lossSocializationAmount,
|
||||
data.decimals
|
||||
);
|
||||
|
||||
return (
|
||||
<WarningCell
|
||||
tooltipContent={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
|
||||
</p>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink href={VEGA_DOCS_URL}>
|
||||
{t('Read more about loss socialisation')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{valueFormatted}
|
||||
</WarningCell>
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenVolumeCell = ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'openVolume'>) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
if (!data) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{valueFormatted}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<WarningCell
|
||||
tooltipContent={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t('Your position was affected by market conditions')}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Status: %s',
|
||||
PositionStatusMapping[
|
||||
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
|
||||
]
|
||||
)}
|
||||
</p>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink href={VEGA_DOCS_URL}>
|
||||
{t('Read more about position resolution')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{valueFormatted}
|
||||
</WarningCell>
|
||||
);
|
||||
};
|
||||
|
||||
const WarningCell = ({
|
||||
children,
|
||||
tooltipContent,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tooltipContent: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip description={tooltipContent}>
|
||||
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
|
||||
<span className="text-black dark:text-white mr-1">
|
||||
<Icon name="warning-sign" size={3} />
|
||||
</span>
|
||||
<span className="text-ellipsis overflow-hidden">{children}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PositionStatus } from '@vegaprotocol/types';
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type {
|
||||
@@ -59,8 +58,6 @@ const positionFields: PositionFieldsFragment[] = [
|
||||
id: 'market-0',
|
||||
__typename: 'Market',
|
||||
},
|
||||
lossSocializationAmount: '0',
|
||||
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
__typename: 'Position',
|
||||
@@ -73,11 +70,8 @@ const positionFields: PositionFieldsFragment[] = [
|
||||
id: 'market-1',
|
||||
__typename: 'Market',
|
||||
},
|
||||
lossSocializationAmount: '0',
|
||||
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
},
|
||||
{
|
||||
__typename: 'Position',
|
||||
realisedPNL: '230000',
|
||||
openVolume: '1',
|
||||
unrealisedPNL: '-22519',
|
||||
@@ -87,8 +81,7 @@ const positionFields: PositionFieldsFragment[] = [
|
||||
id: 'market-2',
|
||||
__typename: 'Market',
|
||||
},
|
||||
lossSocializationAmount: '0',
|
||||
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
__typename: 'Position',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
OrderStatus,
|
||||
OrderTimeInForce,
|
||||
OrderType,
|
||||
PositionStatus,
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
Side,
|
||||
@@ -446,11 +445,3 @@ export const DispatchMetricLabels: DispatchMetricLabel = {
|
||||
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
|
||||
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
|
||||
};
|
||||
|
||||
export const PositionStatusMapping: {
|
||||
[T in PositionStatus]: string;
|
||||
} = {
|
||||
POSITION_STATUS_CLOSED_OUT: 'Closed by network',
|
||||
POSITION_STATUS_ORDERS_CLOSED: 'Maintained by network',
|
||||
POSITION_STATUS_UNSPECIFIED: 'Normal',
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface FormGroupProps {
|
||||
hideLabel?: boolean;
|
||||
labelDescription?: string;
|
||||
labelAlign?: 'left' | 'right';
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const FormGroup = ({
|
||||
@@ -20,16 +19,8 @@ export const FormGroup = ({
|
||||
labelDescription,
|
||||
labelAlign = 'left',
|
||||
hideLabel = false,
|
||||
compact = false,
|
||||
}: FormGroupProps) => {
|
||||
const wrapperClasses = classNames(
|
||||
'relative',
|
||||
{
|
||||
'mb-2': compact,
|
||||
'mb-4': !compact,
|
||||
},
|
||||
className
|
||||
);
|
||||
const wrapperClasses = classNames('relative mb-2', className);
|
||||
const labelClasses = classNames('block mb-2 text-sm', {
|
||||
'text-right': labelAlign === 'right',
|
||||
'sr-only': hideLabel,
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface EthStoredTxState extends EthTxState {
|
||||
methodName: ContractMethod;
|
||||
args: string[];
|
||||
requiredConfirmations: number;
|
||||
requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even)
|
||||
requiresConfirmation: boolean;
|
||||
assetId?: string;
|
||||
deposit?: DepositBusEventFieldsFragment;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user