Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782368285c | ||
|
|
ca24fb0ca3 | ||
|
|
a67f8e0155 | ||
|
|
c637ba86b2 | ||
|
|
faa8bb8481 | ||
|
|
f44013fa94 | ||
|
|
e6cf5bfe1d | ||
|
|
6c72fff23c | ||
|
|
6588327454 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=http://localhost:26617
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
||||
# App flags
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=DEVNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
||||
@@ -2,7 +2,9 @@
|
||||
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_ENV=MIRROR
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
||||
|
||||
@@ -5,6 +5,7 @@ NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
||||
@@ -4,6 +4,7 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
|
||||
|
||||
@@ -3,8 +3,11 @@ NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_NETWORKS={}
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
||||
|
||||
@@ -3,8 +3,10 @@ NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/validator-testnet-network.json
|
||||
NX_VEGA_URL=https://api.validators-testnet.vega.xyz/graphql
|
||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
|
||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
NX_TENDERMINT_URL=http://localhost:26607/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
export const trancheData = {
|
||||
'0': {
|
||||
tranche_id: 0,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'1': {
|
||||
tranche_id: 1,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 1670422330,
|
||||
duration: 1670453883,
|
||||
},
|
||||
'107': {
|
||||
tranche_id: 107,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'110': {
|
||||
tranche_id: 110,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'154': {
|
||||
tranche_id: 154,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
'2': {
|
||||
tranche_id: 2,
|
||||
users: [
|
||||
'0x8716c75bb3abe54b959adc529e6355f0422454ed',
|
||||
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
|
||||
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
|
||||
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
|
||||
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
|
||||
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
|
||||
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
|
||||
'0xf73e7ad8aa300a7f86bcc4d55e6c4ea25546e057',
|
||||
],
|
||||
initial_balance: 111300000000000000000,
|
||||
current_balance: 111297025049610000000,
|
||||
cliff_start: 1677578461,
|
||||
duration: 15209600,
|
||||
},
|
||||
'3': {
|
||||
tranche_id: 3,
|
||||
users: [
|
||||
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
|
||||
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
|
||||
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
|
||||
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
|
||||
'0x77a0b7e247b7b8ec99e068a24c401783d6c4dfff',
|
||||
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
|
||||
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
|
||||
],
|
||||
initial_balance: 21000000000000000000,
|
||||
current_balance: 21000000000000000000,
|
||||
cliff_start: 1677578461,
|
||||
duration: 18628800,
|
||||
},
|
||||
'66': {
|
||||
tranche_id: 66,
|
||||
users: [],
|
||||
initial_balance: 0,
|
||||
current_balance: 0,
|
||||
cliff_start: 0,
|
||||
duration: 0,
|
||||
},
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { trancheData } from '../../fixtures/mocks/tranches';
|
||||
|
||||
const tranches = trancheData;
|
||||
|
||||
context(
|
||||
'Tranches page - verify elements on the page',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('visit homepage', function () {
|
||||
cy.intercept('GET', '**/tranches/stats', { tranches });
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('Able to navigate to tranches page', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.url().should('include', '/token/tranches');
|
||||
cy.get('h1').should('contain.text', 'Vesting tranches');
|
||||
});
|
||||
|
||||
// 1005-VEST-001
|
||||
// 1005-VEST-002
|
||||
it('Able to view tranches', function () {
|
||||
cy.getByTestId('tranche-item')
|
||||
.should('have.length', 2)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 2') // 1005-VEST-003
|
||||
.and('have.attr', 'href', '/token/tranches/2');
|
||||
cy.get('span').eq(1).should('have.text', '111.30'); // 1005-VEST-005
|
||||
cy.contains('Unlocking starts') // 1005-VEST-008
|
||||
.parent()
|
||||
.should('contain.text', '28 Feb 2023');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '23 Aug 2023');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked') // 1005-VEST-006
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('currency-unlocked') // 1005-VEST-007
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to see individual tranche data', function () {
|
||||
cy.get('[href="/token/tranches/2"]').click();
|
||||
cy.getByTestId('redeemed-tranche-tokens').within(() => {
|
||||
cy.get('span').eq(1).should('have.text', 0);
|
||||
});
|
||||
cy.getByTestId('key-value-table').within(() => {
|
||||
cy.getByTestId('link')
|
||||
.should('have.length', 8)
|
||||
.each((ethLink) => {
|
||||
cy.wrap(ethLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/address/');
|
||||
});
|
||||
cy.getByTestId('redeem-link')
|
||||
.should('have.length', 8)
|
||||
.each((redeemLink) => {
|
||||
cy.wrap(redeemLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/token/redeem/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to view tranches with less than 10 vega', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.getByTestId('show-all-tranches').click();
|
||||
cy.getByTestId('tranche-item')
|
||||
.should('have.length', 8)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 0')
|
||||
.and('have.attr', 'href', '/token/tranches/0');
|
||||
cy.get('span').eq(1).should('have.text', '0.00');
|
||||
cy.contains('Unlocking starts')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('currency-unlocked')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,13 @@
|
||||
const connectButton = '[data-testid="connect-to-eth-btn"]';
|
||||
const lockedTokensInVestingContract = '6,499,972.30';
|
||||
|
||||
context(
|
||||
'Vesting Page - verify elements on page',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('navigate to vesting page', function () {
|
||||
cy.visit('/').navigate_to('vesting');
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', function () {
|
||||
before('navigate to vesting page', function () {
|
||||
cy.visit('/').navigate_to('vesting');
|
||||
});
|
||||
it('should have vesting tab highlighted', function () {
|
||||
cy.verify_tab_highlighted('token');
|
||||
});
|
||||
@@ -18,7 +16,10 @@ context(
|
||||
cy.verify_page_header('Vesting');
|
||||
});
|
||||
|
||||
// 1005-VEST-018
|
||||
it('should have connect Eth wallet info', function () {
|
||||
cy.get(connectButton).should('be.visible');
|
||||
});
|
||||
|
||||
it('should have connect Eth wallet button', function () {
|
||||
cy.get(connectButton)
|
||||
.should('be.visible')
|
||||
@@ -26,101 +27,18 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
describe('With Eth wallet connected', function () {
|
||||
describe('with eth wallet connected', function () {
|
||||
before('connect eth wallet', function () {
|
||||
cy.ethereum_wallet_connect();
|
||||
cy.getByTestId('view-connected-eth-btn').click();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
// 1005-VEST-020 1005-VEST-021
|
||||
it('Tokens in vesting contract for eth wallet is displayed on wallet window', function () {
|
||||
cy.getByTestId('vega-in-vesting-contract').within(() => {
|
||||
cy.getByTestId('currency-title')
|
||||
.should('contain.text', 'VEGA')
|
||||
.and('contain.text', 'In vesting contract');
|
||||
cy.getByTestId('currency-value').should(
|
||||
'have.text',
|
||||
lockedTokensInVestingContract
|
||||
);
|
||||
cy.getByTestId('currency-locked').should(
|
||||
'have.text',
|
||||
lockedTokensInVestingContract
|
||||
);
|
||||
cy.getByTestId('currency-unlocked').should('have.text', '0.00');
|
||||
});
|
||||
});
|
||||
// 1005-VEST-022 1005-VEST-023
|
||||
it('Tokens amount displayed in vesting page', function () {
|
||||
cy.getByTestId(
|
||||
'redemption-description',
|
||||
Cypress.env('txTimeout')
|
||||
).should('exist');
|
||||
const redemptionText =
|
||||
'The connected Ethereum wallet (0xEe7D…d94F) has 6,499,972.30 $VEGA tokens in 1 tranche(s) of the vesting contract.';
|
||||
|
||||
cy.getByTestId('redemption-description').should(
|
||||
'have.text',
|
||||
redemptionText
|
||||
);
|
||||
cy.getByTestId('vesting-table').within(() => {
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Vesting VEGA');
|
||||
cy.get('dd').should('have.text', lockedTokensInVestingContract);
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Locked');
|
||||
cy.get('dd').should('have.text', lockedTokensInVestingContract);
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Unlocked');
|
||||
cy.get('dd').should('have.text', '0.00');
|
||||
});
|
||||
cy.getByTestId('key-value-table-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('dt').should('have.text', 'Associated');
|
||||
cy.get('dd').should('have.text', '0.00');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 1005-VEST-024 1005-VEST-025 1005-VEST-026 1005-VEST-036 1005-VEST-037
|
||||
it('Tokens locked in individual tranches are displayed', function () {
|
||||
cy.getByTestId('tranche-table-footer').should(
|
||||
'have.text',
|
||||
'All the tokens in this tranche are locked and must be assigned to a tranche before they can be redeemed.'
|
||||
);
|
||||
cy.getByTestId('tranche-item').within(() => {
|
||||
cy.get('a')
|
||||
.should('have.text', 'Tranche 0')
|
||||
.and('have.attr', 'href', '/token/tranches/0');
|
||||
cy.get('span')
|
||||
.eq(1)
|
||||
.should('have.text', lockedTokensInVestingContract);
|
||||
cy.contains('Unlocking starts') // 1005-VEST-008
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.contains('Fully unlocked')
|
||||
.parent()
|
||||
.should('contain.text', '01 Jan 1970');
|
||||
cy.getByTestId('progress-bar').should('exist');
|
||||
cy.getByTestId('currency-locked') // 1005-VEST-006
|
||||
.should('have.text', lockedTokensInVestingContract);
|
||||
cy.getByTestId('currency-unlocked') // 1005-VEST-007
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('tranche-item-footer').should(
|
||||
'have.text',
|
||||
'All the tokens in this tranche are locked and can not be redeemed yet.'
|
||||
);
|
||||
});
|
||||
cy.connectVegaWallet();
|
||||
// 1005-VEST-001
|
||||
// 1005-VEST-002
|
||||
it('Able to view tranches', function () {
|
||||
cy.navigate_to('supply');
|
||||
cy.url().should('include', '/token/tranches');
|
||||
cy.get('h1').should('contain.text', 'Vesting tranches');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const EthConnectPrompt = () => {
|
||||
const { appDispatch } = useAppState();
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
|
||||
@@ -336,7 +336,6 @@
|
||||
"withdrawLpNoneDeposited": "You have no SLP tokens deposited or rewards accumulated",
|
||||
"withdrawAllLpSuccessCalloutTitle": "Your SLP tokens and rewards have been sent to your Ethereum address",
|
||||
"Dissociate VEGA tokens": "Disassociate $VEGA tokens",
|
||||
"DisassociateVegaTokensFromWallet": "Disassociate $VEGA tokens from your Vega wallet",
|
||||
"Early Investors": "Early Investors",
|
||||
"Team": "Team",
|
||||
"Community": "Community",
|
||||
@@ -483,7 +482,6 @@
|
||||
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
|
||||
"withdrawFormSubmitButtonPending": "Preparing",
|
||||
"withdrawalsTitle": "Withdrawals",
|
||||
"withdrawalsSubtitle": "Connect your Vega wallet to complete a withdrawal. These withdrawals will be completed with an Ethereum transaction.",
|
||||
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
|
||||
"withdrawalsNone": "You don't have any pending withdrawals.",
|
||||
"withdrawalsCompleteButton": "Finish withdrawal",
|
||||
@@ -554,6 +552,9 @@
|
||||
"resourceNotFound": "Resource Not Found",
|
||||
"Or": "Or",
|
||||
"addTokenToWallet": "Show this token in your Ethereum wallet",
|
||||
"chartBelow": "*Certain activities, for example community incentive tokens, do not have specific dates to be credited and so an approximation has been used. ",
|
||||
"chartTitle": "Token unlocking schedule",
|
||||
"chartAbove": "This chart shows the approximate* schedule for how tokens will unlock from vesting tranches over time.",
|
||||
"date": "Date",
|
||||
"noEthereumProviderError": "No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile",
|
||||
"unsupportedChainIdError": "You're connected to an unsupported network",
|
||||
|
||||
@@ -84,7 +84,6 @@ const RedemptionRouter = () => {
|
||||
fill={true}
|
||||
variant="primary"
|
||||
onClick={() => navigate(`${RoutesConfig.REDEEM}/${account}`)}
|
||||
data-testid="view-connected-eth-btn"
|
||||
>
|
||||
{t('View connected Eth Wallet')}
|
||||
</Button>
|
||||
|
||||
@@ -15,12 +15,7 @@ export const DisassociateContainer = () => {
|
||||
<>
|
||||
<Heading title={t('pageTitleDisassociate')} />
|
||||
{!account ? (
|
||||
<>
|
||||
<p className="mb-8">{t('DisassociateVegaTokensFromWallet')}</p>
|
||||
<div className="max-w-[400px]">
|
||||
<EthConnectPrompt />
|
||||
</div>
|
||||
</>
|
||||
<EthConnectPrompt />
|
||||
) : (
|
||||
<DisassociatePage address={account} vegaKey={pubKey ?? ''} />
|
||||
)}
|
||||
|
||||
@@ -73,7 +73,6 @@ export const Tranche = () => {
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
|
||||
import { SubHeading } from '../../components/heading';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
import { VestingChart } from './vesting-chart';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import type { Tranche } from '../../lib/tranches/tranches-store';
|
||||
import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
const trancheMinimum = 10;
|
||||
|
||||
@@ -15,7 +18,7 @@ const shouldShowTranche = (t: Tranche) =>
|
||||
|
||||
export const Tranches = () => {
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const [showAll, setShowAll] = useState<boolean>(false);
|
||||
const [showAll, setShowAll] = React.useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
const { chainId } = useWeb3React();
|
||||
const { config } = useEthereumConfig();
|
||||
@@ -27,6 +30,10 @@ export const Tranches = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SubHeading title={t('chartTitle')} />
|
||||
<p>{t('chartAbove')}</p>
|
||||
<VestingChart />
|
||||
<p>{t('chartBelow')}</p>
|
||||
{tranches?.length ? (
|
||||
<ul role="list">
|
||||
{(showAll ? tranches : filteredTranches).map((tranche) => {
|
||||
@@ -48,12 +55,8 @@ export const Tranches = () => {
|
||||
) : (
|
||||
<p>{t('No tranches')}</p>
|
||||
)}
|
||||
|
||||
<section className="text-center mt-4">
|
||||
<ButtonLink
|
||||
data-testid="show-all-tranches"
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
>
|
||||
<ButtonLink onClick={() => setShowAll(!showAll)}>
|
||||
{showAll
|
||||
? t(
|
||||
'Showing tranches with <{{trancheMinimum}} VEGA, click to hide these tranches',
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { format, startOfMonth } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Label,
|
||||
Legend,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
import { DATE_FORMAT_LONG } from '../../lib/date-formats';
|
||||
import data from './data.json';
|
||||
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
const Colors = theme.colors;
|
||||
|
||||
const ORDER = ['community', 'publicSale', 'earlyInvestors', 'team'];
|
||||
|
||||
export const VestingChart = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentDate = React.useMemo(
|
||||
() => format(startOfMonth(new Date()), DATE_FORMAT_LONG),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div style={{ marginBottom: 25 }}>
|
||||
<ResponsiveContainer height={400} width="100%">
|
||||
<AreaChart data={data}>
|
||||
<defs>
|
||||
{[
|
||||
['pink', Colors.vega.pink.DEFAULT],
|
||||
['green', Colors.vega.green.DEFAULT],
|
||||
['orange', Colors.warning],
|
||||
['yellow', Colors.vega.yellow.DEFAULT],
|
||||
].map(([key, color]) => (
|
||||
<linearGradient key={key} id={key} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.85} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: colors.black }}
|
||||
separator=":"
|
||||
// @ts-ignore formatter doesnt seem to allow returning JSX but nonetheless it works
|
||||
formatter={(value: number) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{Intl.NumberFormat().format(value)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
itemSorter={(label: any) => {
|
||||
return ORDER.indexOf(label.dataKey) + 1;
|
||||
}}
|
||||
/>
|
||||
<YAxis type="number" width={100}>
|
||||
<Label
|
||||
angle={270}
|
||||
value={t('VEGA').toString()}
|
||||
position="left"
|
||||
offset={-5}
|
||||
fill={colors.white}
|
||||
/>
|
||||
</YAxis>
|
||||
<XAxis dataKey="date">
|
||||
<Label
|
||||
value={t('date').toString()}
|
||||
position="bottom"
|
||||
offset={5}
|
||||
fill={colors.white}
|
||||
/>
|
||||
</XAxis>
|
||||
<ReferenceLine
|
||||
x={currentDate}
|
||||
stroke={colors.white}
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
position: 'left',
|
||||
value: currentDate,
|
||||
fill: colors.white,
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
dot={false}
|
||||
type="linear"
|
||||
dataKey="team"
|
||||
stroke={Colors.vega.pink.DEFAULT}
|
||||
fill="url(#pink)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.85}
|
||||
stackId="1"
|
||||
name={t('Team')}
|
||||
/>
|
||||
<Area
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="earlyInvestors"
|
||||
stroke={Colors.vega.green.DEFAULT}
|
||||
fill="url(#green)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.85}
|
||||
stackId="1"
|
||||
name={t('Early Investors')}
|
||||
/>
|
||||
<Area
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="publicSale"
|
||||
stroke={Colors.vega.yellow.DEFAULT}
|
||||
fill="url(#yellow)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
stackId="1"
|
||||
fillOpacity={0.85}
|
||||
name={t('Public Sale')}
|
||||
/>
|
||||
<Area
|
||||
dot={false}
|
||||
type="monotone"
|
||||
dataKey="community"
|
||||
stroke={Colors.warning}
|
||||
fill="url(#orange)"
|
||||
yAxisId={0}
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.85}
|
||||
stackId="1"
|
||||
name={t('Community')}
|
||||
/>
|
||||
<Legend wrapperStyle={{ position: 'relative' }} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,14 +17,12 @@ import type { RouteChildProps } from '../index';
|
||||
const Withdrawals = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading title={t('withdrawalsTitle')} />
|
||||
{!pubKey && <p className="mb-8">{t('withdrawalsSubtitle')}</p>}
|
||||
<VegaWalletContainer>
|
||||
{() => <WithdrawPendingContainer />}
|
||||
{(currVegaKey) => <WithdrawPendingContainer />}
|
||||
</VegaWalletContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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', () => {
|
||||
|
||||
@@ -669,7 +669,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
'have.text',
|
||||
'You need ' +
|
||||
'tDAI' +
|
||||
' in your wallet to trade in this market.See all your collateral.Make a deposit'
|
||||
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -111,11 +111,11 @@ export const TradeMarketHeader = ({
|
||||
</div>
|
||||
</HeaderStat>
|
||||
) : null}
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market?.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
/>
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.'
|
||||
)}
|
||||
|
||||
@@ -61,9 +61,10 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
<div className="ag-filter-body-wrapper">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
{Object.keys(props.colDef.filterParams.set).map((key) => (
|
||||
<label className="flex" key={key}>
|
||||
<label className="flex">
|
||||
<input
|
||||
type="checkbox"
|
||||
key={key}
|
||||
value={key}
|
||||
className="mr-1"
|
||||
checked={value.includes(key)}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { StateCreator } from 'zustand';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
const { create: actualCreate } = jest.requireActual('zustand'); // if using jest
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
const storeResetFns = new Set<() => void>();
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const create =
|
||||
() =>
|
||||
<S>(createState: StateCreator<S>) => {
|
||||
const store = actualCreate(createState);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => store.setState(initialState, true));
|
||||
return store;
|
||||
};
|
||||
|
||||
// Reset all stores after each test run
|
||||
beforeEach(() => {
|
||||
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||
});
|
||||
@@ -7,7 +7,7 @@ interface ZeroBalanceErrorProps {
|
||||
id: string;
|
||||
symbol: string;
|
||||
};
|
||||
onClickCollateral?: () => void;
|
||||
onClickCollateral: () => void;
|
||||
}
|
||||
|
||||
export const ZeroBalanceError = ({
|
||||
@@ -21,12 +21,8 @@ export const ZeroBalanceError = ({
|
||||
testId="dealticket-error-message-zero-balance"
|
||||
message={
|
||||
<>
|
||||
You need {asset.symbol} in your wallet to trade in this market.
|
||||
{onClickCollateral && (
|
||||
<>
|
||||
See all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||
</>
|
||||
)}
|
||||
You need {asset.symbol} in your wallet to trade in this market. See
|
||||
all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||
</>
|
||||
}
|
||||
buttonProps={{
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import type { Control } from 'react-hook-form';
|
||||
import type { UseFormRegister } from 'react-hook-form';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import type { OrderFormFields } from '../../hooks/use-order-form';
|
||||
import type { DealTicketFormFields } from './deal-ticket';
|
||||
|
||||
export interface DealTicketAmountProps {
|
||||
control: Control<OrderFormFields>;
|
||||
orderType: Schema.OrderType;
|
||||
marketData: MarketData;
|
||||
market: Market;
|
||||
register: UseFormRegister<DealTicketFormFields>;
|
||||
sizeError?: string;
|
||||
priceError?: string;
|
||||
update: (obj: Partial<OrderObj>) => void;
|
||||
size: string;
|
||||
price?: string;
|
||||
}
|
||||
|
||||
export const DealTicketAmount = ({
|
||||
|
||||
@@ -47,7 +47,7 @@ export const DealTicketContainer = ({
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
onClickCollateral={onClickCollateral}
|
||||
onClickCollateral={onClickCollateral || (() => null)}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketLimitAmountProps = Omit<
|
||||
Omit<DealTicketAmountProps, 'marketData'>,
|
||||
@@ -10,13 +9,10 @@ export type DealTicketLimitAmountProps = Omit<
|
||||
>;
|
||||
|
||||
export const DealTicketLimitAmount = ({
|
||||
control,
|
||||
register,
|
||||
market,
|
||||
sizeError,
|
||||
priceError,
|
||||
update,
|
||||
price,
|
||||
size,
|
||||
}: DealTicketLimitAmountProps) => {
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -51,30 +47,22 @@ export const DealTicketLimitAmount = ({
|
||||
labelFor="input-order-size-limit"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...register('size', {
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-limit"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
data-testid="order-size"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
/>
|
||||
)}
|
||||
})}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
@@ -89,10 +77,14 @@ export const DealTicketLimitAmount = ({
|
||||
labelAlign="right"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="price"
|
||||
control={control}
|
||||
rules={{
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...register('price', {
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
@@ -100,19 +92,7 @@ export const DealTicketLimitAmount = ({
|
||||
},
|
||||
// @ts-ignore this fulfills the interface but still errors
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-price-quote"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={price}
|
||||
onChange={(e) => update({ price: e.target.value })}
|
||||
step={priceStep}
|
||||
data-testid="order-price"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
/>
|
||||
)}
|
||||
})}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { isMarketInAuction } from '../../utils';
|
||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||
import { getMarketPrice } from '../../utils/get-price';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
export type DealTicketMarketAmountProps = Omit<
|
||||
DealTicketAmountProps,
|
||||
@@ -16,12 +15,10 @@ export type DealTicketMarketAmountProps = Omit<
|
||||
>;
|
||||
|
||||
export const DealTicketMarketAmount = ({
|
||||
control,
|
||||
register,
|
||||
market,
|
||||
marketData,
|
||||
sizeError,
|
||||
update,
|
||||
size,
|
||||
}: DealTicketMarketAmountProps) => {
|
||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
@@ -50,30 +47,22 @@ export const DealTicketMarketAmount = ({
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
name="size"
|
||||
control={control}
|
||||
rules={{
|
||||
<Input
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
{...register('size', {
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
render={() => (
|
||||
<Input
|
||||
id="input-order-size-market"
|
||||
className="w-full"
|
||||
type="number"
|
||||
value={size}
|
||||
onChange={(e) => update({ size: e.target.value })}
|
||||
step={sizeStep}
|
||||
min={sizeStep}
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
data-testid="order-size"
|
||||
/>
|
||||
)}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>@</div>
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent, render, screen, act } from '@testing-library/react';
|
||||
import { generateMarket, generateMarketData } from '../../test-helpers';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
|
||||
import { ChainIdDocument } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { useOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
jest.mock('zustand');
|
||||
jest.mock('./deal-ticket-fee-details', () => ({
|
||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||
}));
|
||||
|
||||
const pubKey = 'pubKey';
|
||||
const market = generateMarket();
|
||||
const marketData = generateMarketData();
|
||||
const submit = jest.fn();
|
||||
|
||||
function generateJsx() {
|
||||
const mockChainId = 'chain-id';
|
||||
|
||||
function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
||||
const chainIdMock: MockedResponse<ChainIdQuery> = {
|
||||
request: {
|
||||
query: ChainIdDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
chainId: mockChainId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
|
||||
<MockedProvider mocks={[chainIdMock]}>
|
||||
<VegaWalletContext.Provider value={{ pubKey: mockChainId } as any}>
|
||||
<DealTicket market={market} marketData={marketData} submit={submit} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
@@ -31,11 +41,10 @@ function generateJsx() {
|
||||
|
||||
describe('DealTicket', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
window.localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -52,7 +61,9 @@ describe('DealTicket', () => {
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
String(1 / Math.pow(10, market.positionDecimalPlaces))
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
@@ -65,49 +76,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use local storage state for initial values', () => {
|
||||
const expectedOrder = {
|
||||
marketId: market.id,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
persist: true,
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
expectedOrder.timeInForce
|
||||
);
|
||||
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
|
||||
expectedOrder.price
|
||||
);
|
||||
});
|
||||
|
||||
it('handles TIF select box dependent on order type', async () => {
|
||||
it('handles TIF select box dependent on order type', () => {
|
||||
render(generateJsx());
|
||||
|
||||
// Only FOK and IOC should be present by default (type market order)
|
||||
@@ -117,72 +86,50 @@ describe('DealTicket', () => {
|
||||
)
|
||||
).toEqual(['Fill or Kill (FOK)', 'Immediate or Cancel (IOC)']);
|
||||
|
||||
// IOC should be default
|
||||
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||
'Immediate or Cancel (IOC)'
|
||||
);
|
||||
|
||||
// Select FOK - FOK should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveDisplayValue(
|
||||
'Fill or Kill (FOK)'
|
||||
);
|
||||
|
||||
// Switch to type limit order -> all TIF options should be shown
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
Object.keys(Schema.OrderTimeInForce).length
|
||||
);
|
||||
|
||||
// expect GTC as LIMIT default
|
||||
// Select GTC -> GTC should be selected
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be selected (default)
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Select IOC -> IOC should be selected
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch to type limit order -> GTC should be selected
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||
);
|
||||
|
||||
// Select GTT -> GTT should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
|
||||
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
|
||||
// Select IOC -> IOC should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch back type limit order -> GTT should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
|
||||
// Select GFN -> GFN should be selected
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN
|
||||
);
|
||||
|
||||
// Switch to type market order -> IOC should be preserved
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
// Switch to type market order -> IOC should be selected
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
@@ -196,20 +143,23 @@ describe('DealTicket', () => {
|
||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
|
||||
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('order-size'), {
|
||||
target: { value: '200' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('order-tif'),
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
fireEvent.change(screen.getByTestId('order-tif'), {
|
||||
target: { value: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC },
|
||||
});
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
|
||||
// Switch to limit order
|
||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
||||
|
||||
// Check all TIF options shown
|
||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
@@ -9,12 +9,10 @@ import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
normalizeOrderSubmission,
|
||||
useVegaWallet,
|
||||
useVegaWalletDialogStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
ExternalLink,
|
||||
InputError,
|
||||
@@ -24,7 +22,7 @@ import {
|
||||
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import {
|
||||
validateExpiration,
|
||||
getDefaultOrder,
|
||||
validateMarketState,
|
||||
validateMarketTradingMode,
|
||||
validateTimeInForce,
|
||||
@@ -34,17 +32,27 @@ import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||
import { SummaryValidationType } from '../../constants';
|
||||
import { useHasNoBalance } from '../../hooks/use-has-no-balance';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import {
|
||||
usePersistedOrderStore,
|
||||
usePersistedOrderStoreSubscription,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
|
||||
export type TransactionStatus = 'default' | 'pending';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
|
||||
onClickCollateral?: () => void;
|
||||
}
|
||||
|
||||
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
||||
// This is not a field used in the form but allows us to set a
|
||||
// summary error message
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
marketData,
|
||||
@@ -52,21 +60,44 @@ export const DealTicket = ({
|
||||
onClickCollateral,
|
||||
}: DealTicketProps) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
// store last used tif for market so that when changing OrderType the previous TIF
|
||||
// selection for that type is used when switching back
|
||||
const [lastTIF, setLastTIF] = useState({
|
||||
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
});
|
||||
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
|
||||
(store) => ({
|
||||
getPersistedOrder: store.getOrder,
|
||||
setPersistedOrder: store.setOrder,
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
errors,
|
||||
order,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setError,
|
||||
clearErrors,
|
||||
update,
|
||||
handleSubmit,
|
||||
} = useOrderForm(market.id);
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useForm<DealTicketFormFields>({
|
||||
defaultValues: getPersistedOrder(market.id) || getDefaultOrder(market),
|
||||
});
|
||||
|
||||
const order = watch();
|
||||
|
||||
watch((orderData) => {
|
||||
const persistable = !(
|
||||
orderData.type === OrderType.TYPE_LIMIT && orderData.price === ''
|
||||
);
|
||||
if (persistable) {
|
||||
setPersistedOrder(orderData as DealTicketFormFields);
|
||||
}
|
||||
});
|
||||
|
||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
||||
if (order.price !== storedOrder.price) {
|
||||
clearErrors('price');
|
||||
setValue('price', storedOrder.price);
|
||||
}
|
||||
});
|
||||
|
||||
const marketStateError = validateMarketState(marketData.marketState);
|
||||
const hasNoBalance = useHasNoBalance(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
@@ -135,7 +166,7 @@ export const DealTicket = ({
|
||||
]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmission) => {
|
||||
(order: OrderSubmissionBody['orderSubmission']) => {
|
||||
checkForErrors();
|
||||
submit(
|
||||
normalizeOrderSubmission(
|
||||
@@ -148,12 +179,9 @@ export const DealTicket = ({
|
||||
[checkForErrors, submit, market.decimalPlaces, market.positionDecimalPlaces]
|
||||
);
|
||||
|
||||
// if an order doesn't exist one will be created by the store immediately
|
||||
if (!order) return null;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
||||
onSubmit={isReadOnly ? () => null : handleSubmit(onSubmit)}
|
||||
className="p-4"
|
||||
noValidate
|
||||
>
|
||||
@@ -166,17 +194,10 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<TypeSelector
|
||||
value={order.type}
|
||||
onSelect={(type) => {
|
||||
if (type === OrderType.TYPE_NETWORK) return;
|
||||
update({
|
||||
type,
|
||||
// when changing type also update the tif to what was last used of new type
|
||||
timeInForce: lastTIF[type] || order.timeInForce,
|
||||
});
|
||||
}}
|
||||
value={field.value}
|
||||
onSelect={field.onChange}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.type?.message}
|
||||
@@ -186,25 +207,17 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={() => (
|
||||
<SideSelector
|
||||
value={order.side}
|
||||
onSelect={(side) => {
|
||||
update({ side });
|
||||
}}
|
||||
/>
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onSelect={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<DealTicketAmount
|
||||
control={control}
|
||||
orderType={order.type}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
register={register}
|
||||
sizeError={errors.size?.message}
|
||||
priceError={errors.price?.message}
|
||||
update={update}
|
||||
size={order.size}
|
||||
price={order.price}
|
||||
/>
|
||||
<Controller
|
||||
name="timeInForce"
|
||||
@@ -215,16 +228,11 @@ export const DealTicket = ({
|
||||
marketData.trigger
|
||||
),
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<TimeInForceSelector
|
||||
value={order.timeInForce}
|
||||
value={field.value}
|
||||
orderType={order.type}
|
||||
onSelect={(timeInForce) => {
|
||||
update({ timeInForce });
|
||||
// Set tif value for the given order type, so that when switching
|
||||
// types we know the last used TIF for the given order type
|
||||
setLastTIF((curr) => ({ ...curr, [order.type]: timeInForce }));
|
||||
}}
|
||||
onSelect={field.onChange}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
errorMessage={errors.timeInForce?.message}
|
||||
@@ -236,18 +244,12 @@ export const DealTicket = ({
|
||||
<Controller
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: validateExpiration,
|
||||
}}
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<ExpirySelector
|
||||
value={order.expiresAt}
|
||||
onSelect={(expiresAt) =>
|
||||
update({
|
||||
expiresAt: expiresAt || undefined,
|
||||
})
|
||||
}
|
||||
value={field.value}
|
||||
onSelect={field.onChange}
|
||||
errorMessage={errors.expiresAt?.message}
|
||||
register={register}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -259,7 +261,7 @@ export const DealTicket = ({
|
||||
order={order}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onClickCollateral={onClickCollateral}
|
||||
onClickCollateral={onClickCollateral || (() => null)}
|
||||
/>
|
||||
<DealTicketButton
|
||||
disabled={Object.keys(errors).length >= 1 || isReadOnly}
|
||||
@@ -282,10 +284,10 @@ interface SummaryMessageProps {
|
||||
errorMessage?: string;
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
order: OrderObj;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
isReadOnly: boolean;
|
||||
pubKey: string | null;
|
||||
onClickCollateral?: () => void;
|
||||
onClickCollateral: () => void;
|
||||
}
|
||||
const SummaryMessage = memo(
|
||||
({
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { UseFormRegister } from 'react-hook-form';
|
||||
import { validateExpiration } from '../../utils/validate-expiration';
|
||||
import type { DealTicketFormFields } from '.';
|
||||
|
||||
interface ExpirySelectorProps {
|
||||
value?: string;
|
||||
onSelect: (expiration: string | null) => void;
|
||||
errorMessage?: string;
|
||||
register?: UseFormRegister<DealTicketFormFields>;
|
||||
}
|
||||
|
||||
export const ExpirySelector = ({
|
||||
value,
|
||||
onSelect,
|
||||
errorMessage,
|
||||
register,
|
||||
}: ExpirySelectorProps) => {
|
||||
const date = value ? new Date(value) : new Date();
|
||||
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"
|
||||
@@ -29,6 +30,9 @@ export const ExpirySelector = ({
|
||||
value={dateFormatted}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={minDate}
|
||||
{...register?.('expiresAt', {
|
||||
validate: validateExpiration,
|
||||
})}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<InputError testId="dealticket-error-message-expiry">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
InputError,
|
||||
@@ -21,6 +22,15 @@ interface TimeInForceSelectorProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
type OrderType = Schema.OrderType.TYPE_MARKET | Schema.OrderType.TYPE_LIMIT;
|
||||
type PreviousTimeInForce = {
|
||||
[key in OrderType]: Schema.OrderTimeInForce;
|
||||
};
|
||||
const DEFAULT_TIME_IN_FORCE: PreviousTimeInForce = {
|
||||
[Schema.OrderType.TYPE_MARKET]: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
[Schema.OrderType.TYPE_LIMIT]: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
};
|
||||
|
||||
export const TimeInForceSelector = ({
|
||||
value,
|
||||
orderType,
|
||||
@@ -37,6 +47,28 @@ export const TimeInForceSelector = ({
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
|
||||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||
);
|
||||
const [previousOrderType, setPreviousOrderType] = useState(
|
||||
Schema.OrderType.TYPE_MARKET
|
||||
);
|
||||
const [previousTimeInForce, setPreviousTimeInForce] =
|
||||
useState<PreviousTimeInForce>({
|
||||
...DEFAULT_TIME_IN_FORCE,
|
||||
[orderType]: value,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (previousOrderType !== orderType) {
|
||||
setPreviousOrderType(orderType);
|
||||
const prev = previousTimeInForce[orderType as OrderType];
|
||||
onSelect(prev);
|
||||
}
|
||||
}, [
|
||||
onSelect,
|
||||
orderType,
|
||||
previousTimeInForce,
|
||||
previousOrderType,
|
||||
setPreviousOrderType,
|
||||
]);
|
||||
|
||||
const renderError = (errorType: string) => {
|
||||
if (errorType === MarketModeValidationType.Auction) {
|
||||
@@ -87,25 +119,15 @@ 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}
|
||||
onChange={(e) => {
|
||||
// setPreviousTimeInForce({
|
||||
// ...previousTimeInForce,
|
||||
// [orderType]: e.target.value,
|
||||
// });
|
||||
|
||||
// if (previousOrderType !== orderType) {
|
||||
// setPreviousOrderType(orderType);
|
||||
// const prev = previousTimeInForce[orderType as OrderType];
|
||||
// onSelect(prev);
|
||||
// }
|
||||
setPreviousTimeInForce({
|
||||
...previousTimeInForce,
|
||||
[orderType]: e.target.value,
|
||||
});
|
||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||
}}
|
||||
className="w-full"
|
||||
|
||||
@@ -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,66 +0,0 @@
|
||||
import omit from 'lodash/omit';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { getDefaultOrder, useOrderStore } from '@vegaprotocol/orders';
|
||||
import { useOrderForm } from './use-order-form';
|
||||
|
||||
jest.mock('zustand');
|
||||
|
||||
describe('useOrderForm', () => {
|
||||
const marketId = 'market-id';
|
||||
const setup = (marketId: string) => {
|
||||
return renderHook(() => useOrderForm(marketId));
|
||||
};
|
||||
|
||||
it('updates form fields when the order changes', async () => {
|
||||
const order = getDefaultOrder(marketId);
|
||||
const { result } = setup(marketId);
|
||||
// expect default values
|
||||
expect(result.current.order).toEqual(order);
|
||||
expect(result.current.getValues()).toEqual(order);
|
||||
|
||||
const priceUpdate = {
|
||||
...order,
|
||||
price: '100',
|
||||
size: '22',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: priceUpdate,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// check order store has updated fields
|
||||
expect(result.current.order).toEqual(priceUpdate);
|
||||
// check react-hook-form has updated fields
|
||||
expect(result.current.getValues()).toEqual(priceUpdate);
|
||||
});
|
||||
|
||||
it('removes persist key on submit', async () => {
|
||||
const order = {
|
||||
...getDefaultOrder(marketId),
|
||||
price: '99',
|
||||
size: '22',
|
||||
};
|
||||
const onSubmit = jest.fn();
|
||||
const { result } = setup(marketId);
|
||||
|
||||
await act(async () => {
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[marketId]: order,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSubmit(onSubmit)();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit.mock.calls[0][0]).toEqual(omit(order, 'persist'));
|
||||
expect(onSubmit.mock.calls[0][0].persist).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
import omit from 'lodash/omit';
|
||||
import type { OrderObj } from '@vegaprotocol/orders';
|
||||
import { getDefaultOrder, useOrder } from '@vegaprotocol/orders';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import type { Exact } from 'type-fest';
|
||||
|
||||
export type OrderFormFields = OrderObj & {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects the order store to a react-hook-form instance. Any time a field
|
||||
* changes in the store the form will be updated so that validation rules
|
||||
* for those fields are applied
|
||||
*/
|
||||
export const useOrderForm = (marketId: string) => {
|
||||
const [order, update] = useOrder(marketId);
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitted },
|
||||
handleSubmit,
|
||||
setError,
|
||||
setValue,
|
||||
clearErrors,
|
||||
getValues,
|
||||
} = useForm<OrderFormFields>({
|
||||
// order can be undefined if there is nothing in the store, it
|
||||
// will be created but the form still needs some default values
|
||||
defaultValues: order || getDefaultOrder(marketId),
|
||||
});
|
||||
|
||||
// Keep form fields in sync with the store values,
|
||||
// inputs are updating the store, fields need updating
|
||||
// to ensure validation rules are applied
|
||||
useEffect(() => {
|
||||
if (!order) return;
|
||||
const currOrder = getValues();
|
||||
for (const k in order) {
|
||||
const key = k as keyof typeof order;
|
||||
const curr = currOrder[key];
|
||||
const value = order[key];
|
||||
if (value !== curr) {
|
||||
setValue(key, value, {
|
||||
shouldValidate: isSubmitted, // only apply validation after the form has been submitted and failed
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [order, isSubmitted, getValues, setValue]);
|
||||
|
||||
const handleSubmitWrapper = (
|
||||
cb: <T>(o: Exact<OrderSubmission, T>) => void
|
||||
) => {
|
||||
return handleSubmit(() => {
|
||||
// remove the persist key from the order in the store, the wallet will reject
|
||||
// an order that contains unrecognized additional keys
|
||||
cb(omit(order, 'persist'));
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
order,
|
||||
update,
|
||||
control,
|
||||
errors,
|
||||
setError,
|
||||
clearErrors,
|
||||
getValues, // returned for test purposes only
|
||||
handleSubmit: handleSubmitWrapper,
|
||||
};
|
||||
};
|
||||
@@ -17,8 +17,7 @@
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"jest.config.ts",
|
||||
"__mocks__"
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@ export const RowData = ({
|
||||
highestBlock,
|
||||
onBlockHeight,
|
||||
}: RowDataProps) => {
|
||||
const [subFailed, setSubFailed] = useState(false);
|
||||
const [time, setTime] = useState<number>();
|
||||
// no use of data here as we need the data nodes reference to block height
|
||||
const { data, error, loading, startPolling, stopPolling } =
|
||||
@@ -47,18 +46,9 @@ export const RowData = ({
|
||||
} = useBlockTimeSubscription();
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!subData) {
|
||||
setSubFailed(true);
|
||||
}
|
||||
}, 3000);
|
||||
// stop polling if row has errored
|
||||
}, [error, stopPolling]);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [subData]);
|
||||
|
||||
// handle polling
|
||||
useEffect(() => {
|
||||
const handleStartPoll = () => startPolling(POLL_INTERVAL);
|
||||
const handleStopPoll = () => stopPolling();
|
||||
@@ -78,7 +68,6 @@ export const RowData = ({
|
||||
};
|
||||
}, [startPolling, stopPolling, error]);
|
||||
|
||||
// measure response time
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
// every time we get data measure response speed
|
||||
@@ -142,15 +131,6 @@ export const RowData = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
const getSubFailed = (
|
||||
subError: ApolloError | undefined,
|
||||
subFailed: boolean
|
||||
) => {
|
||||
if (subError) return true;
|
||||
if (subFailed) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{id !== CUSTOM_NODE_KEY && (
|
||||
@@ -181,11 +161,11 @@ export const RowData = ({
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Subscription')}
|
||||
isLoading={subFailed ? false : subLoading}
|
||||
hasError={getSubFailed(subError, subFailed)}
|
||||
isLoading={subLoading}
|
||||
hasError={Boolean(subError)}
|
||||
dataTestId="subscription-cell"
|
||||
>
|
||||
{getSubscriptionDisplayValue(subFailed, subData?.busEvents, subError)}
|
||||
{getSubscriptionDisplayValue(subData?.busEvents, subError)}
|
||||
</LayoutCell>
|
||||
</>
|
||||
);
|
||||
@@ -215,11 +195,10 @@ const getBlockDisplayValue = (block?: number, error?: ApolloError) => {
|
||||
};
|
||||
|
||||
const getSubscriptionDisplayValue = (
|
||||
subFailed: boolean,
|
||||
events?: { id: string }[] | null,
|
||||
error?: ApolloError
|
||||
) => {
|
||||
if (subFailed || error) {
|
||||
if (error) {
|
||||
return t('No');
|
||||
}
|
||||
if (events?.length) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"**/__mocks__/*.tsx",
|
||||
"jest.config.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
getPriceLevel,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import { useOrderStore } from '@vegaprotocol/orders';
|
||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
@@ -172,7 +172,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
flush();
|
||||
}, [resolution, flush]);
|
||||
|
||||
const updateOrder = useOrderStore((store) => store.update);
|
||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
@@ -190,7 +190,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
onClick={(price?: string | number) => {
|
||||
if (price) {
|
||||
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
|
||||
updateOrder(marketId, { price: priceValue });
|
||||
updatePrice(marketId, priceValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { StateCreator } from 'zustand';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
const { create: actualCreate } = jest.requireActual('zustand'); // if using jest
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
const storeResetFns = new Set<() => void>();
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
export const create =
|
||||
() =>
|
||||
<S>(createState: StateCreator<S>) => {
|
||||
const store = actualCreate(createState);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => store.setState(initialState, true));
|
||||
return store;
|
||||
};
|
||||
|
||||
// Reset all stores after each test run
|
||||
beforeEach(() => {
|
||||
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||
});
|
||||
@@ -8,10 +8,6 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
||||
// avoid warnings about padding refs
|
||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||
|
||||
const generateJsx = () => {
|
||||
const pubKey = '0x123';
|
||||
return (
|
||||
@@ -60,6 +56,9 @@ describe('OrderListManager', () => {
|
||||
});
|
||||
|
||||
it('should render the order list if orders provided', async () => {
|
||||
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
||||
// avoid warnings about padding refs
|
||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [{ id: '1' } as OrderFieldsFragment],
|
||||
loading: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import type {
|
||||
BodyScrollEvent,
|
||||
BodyScrollEndEvent,
|
||||
@@ -98,60 +98,38 @@ export const OrderListManager = ({
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const onBodyScrollEnd = useCallback(
|
||||
(event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
}
|
||||
},
|
||||
[addNewRows]
|
||||
);
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
}
|
||||
};
|
||||
|
||||
const onBodyScroll = useCallback((event: BodyScrollEvent) => {
|
||||
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||
scrolledToTop.current = event.top <= 0;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
const updatedFilter = event.api.getFilterModel();
|
||||
if (Object.keys(updatedFilter).length) {
|
||||
setFilter(updatedFilter);
|
||||
} else {
|
||||
setFilter(undefined);
|
||||
}
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
const onFilterChanged = (event: FilterChangedEvent) => {
|
||||
const updatedFilter = event.api.getFilterModel();
|
||||
if (Object.keys(updatedFilter).length) {
|
||||
setFilter(updatedFilter);
|
||||
} else if (filter) {
|
||||
setFilter(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onSortChange = useCallback(
|
||||
(event: SortChangedEvent) => {
|
||||
const sort = event.columnApi
|
||||
.getColumnState()
|
||||
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
|
||||
.reduce((acc, col) => {
|
||||
if (col.sort) {
|
||||
const { colId, sort } = col;
|
||||
acc.push({ colId, sort });
|
||||
}
|
||||
return acc;
|
||||
}, [] as { colId: string; sort: string }[]);
|
||||
setSort(sort.length > 0 ? sort : undefined);
|
||||
},
|
||||
[setSort]
|
||||
);
|
||||
|
||||
const onCancel = useCallback(
|
||||
(order: Order) => {
|
||||
if (!order.market) return;
|
||||
create({
|
||||
orderCancellation: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
[create]
|
||||
);
|
||||
const onSortChange = (event: SortChangedEvent) => {
|
||||
const sort = event.columnApi
|
||||
.getColumnState()
|
||||
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
|
||||
.reduce((acc, col) => {
|
||||
if (col.sort) {
|
||||
const { colId, sort } = col;
|
||||
acc.push({ colId, sort });
|
||||
}
|
||||
return acc;
|
||||
}, [] as { colId: string; sort: string }[]);
|
||||
setSort(sort.length > 0 ? sort : undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -165,7 +143,15 @@ export const OrderListManager = ({
|
||||
onBodyScroll={onBodyScroll}
|
||||
onFilterChanged={onFilterChanged}
|
||||
onSortChanged={onSortChange}
|
||||
cancel={onCancel}
|
||||
cancel={(order: Order) => {
|
||||
if (!order.market) return;
|
||||
create({
|
||||
orderCancellation: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
setEditOrder={setEditOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
isReadOnly={isReadOnly}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { PartialDeep } from 'type-fest';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
import type { OrderListTableProps } from '../';
|
||||
import { OrderListTable } from '../';
|
||||
import {
|
||||
@@ -14,15 +15,6 @@ import {
|
||||
marketOrder,
|
||||
} from '../mocks/generate-orders';
|
||||
|
||||
// Mock theme switcher to get around inconsistent mocking of zustand
|
||||
// stores
|
||||
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||
useThemeSwitcher: () => ({
|
||||
theme: 'light',
|
||||
}),
|
||||
}));
|
||||
|
||||
const defaultProps: OrderListTableProps = {
|
||||
rowData: [],
|
||||
setEditOrder: jest.fn(),
|
||||
|
||||
@@ -91,9 +91,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'size'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (!data?.market || !isNumeric(value)) {
|
||||
return '-';
|
||||
}
|
||||
@@ -119,9 +116,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'type'>) => {
|
||||
if (!order) {
|
||||
return undefined;
|
||||
}
|
||||
if (!value) return '-';
|
||||
if (order?.peggedOrder) return t('Pegged');
|
||||
if (order?.liquidityProvision) return t('Liquidity provision');
|
||||
@@ -168,9 +162,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'remaining'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
|
||||
return '-';
|
||||
}
|
||||
@@ -193,9 +184,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Order, 'price'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!data?.market ||
|
||||
data.type === Schema.OrderType.TYPE_MARKET ||
|
||||
@@ -249,9 +237,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
data,
|
||||
value,
|
||||
}: VegaICellRendererParams<Order, 'updatedAt'>) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
<span data-value={value}>
|
||||
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
|
||||
|
||||
@@ -4,4 +4,4 @@ export * from './use-order-cancel';
|
||||
export * from './use-order-submit';
|
||||
export * from './use-order-edit';
|
||||
export * from './use-order-update';
|
||||
export * from './use-order-store';
|
||||
export * from './use-persisted-order';
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import {
|
||||
getDefaultOrder,
|
||||
STORAGE_KEY,
|
||||
useOrder,
|
||||
useOrderStore,
|
||||
} from './use-order-store';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
|
||||
jest.mock('zustand');
|
||||
|
||||
describe('useOrderStore', () => {
|
||||
const setup = () => {
|
||||
return renderHook(() => useOrderStore());
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('has a empty default state', async () => {
|
||||
const { result } = setup();
|
||||
expect(result.current).toEqual({
|
||||
orders: {},
|
||||
update: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('can update', () => {
|
||||
const marketId = 'persisted-market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
persist: true,
|
||||
};
|
||||
const { result } = setup();
|
||||
act(() => {
|
||||
result.current.update(marketId, { type: OrderType.TYPE_LIMIT });
|
||||
});
|
||||
// order should be stored in memory
|
||||
expect(result.current.orders).toEqual({
|
||||
[marketId]: expectedOrder,
|
||||
});
|
||||
// order SHOULD also be in localStorage
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
|
||||
state: {
|
||||
orders: {
|
||||
[marketId]: expectedOrder,
|
||||
},
|
||||
},
|
||||
version: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('can update without persisting', () => {
|
||||
const marketId = 'non-persisted-market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup();
|
||||
act(() => {
|
||||
result.current.update(marketId, { type: OrderType.TYPE_LIMIT }, false);
|
||||
});
|
||||
// order should be stored in memory
|
||||
expect(result.current.orders).toEqual({
|
||||
[marketId]: expectedOrder,
|
||||
});
|
||||
// order should NOT be in localStorage
|
||||
expect(JSON.parse(localStorage.getItem(STORAGE_KEY) || '')).toEqual({
|
||||
state: {
|
||||
orders: {},
|
||||
},
|
||||
version: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOrder', () => {
|
||||
const setup = (marketId: string) => {
|
||||
return renderHook(() => useOrder(marketId));
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('creates a new order if it doesnt exist which is only persisted after editing', () => {
|
||||
const marketId = 'market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup(marketId);
|
||||
expect(result.current).toEqual([expectedOrder, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('only persists an order if edited', () => {
|
||||
const marketId = 'market-id';
|
||||
const expectedOrder = {
|
||||
...getDefaultOrder(marketId),
|
||||
persist: false,
|
||||
};
|
||||
const { result } = setup(marketId);
|
||||
expect(result.current[0]).toMatchObject({
|
||||
price: expectedOrder.price,
|
||||
persist: false,
|
||||
});
|
||||
|
||||
const update = { price: '500' };
|
||||
act(() => {
|
||||
result.current[1](update);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toMatchObject({
|
||||
...update,
|
||||
persist: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
import { OrderTimeInForce, Side } from '@vegaprotocol/types';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
export type OrderObj = {
|
||||
marketId: string;
|
||||
type: OrderType;
|
||||
side: Side;
|
||||
size: string;
|
||||
timeInForce: OrderTimeInForce;
|
||||
price?: string;
|
||||
expiresAt?: string | undefined;
|
||||
persist: boolean; // key used to determine if order should be kept in localStorage
|
||||
};
|
||||
type OrderMap = { [marketId: string]: OrderObj | undefined };
|
||||
|
||||
type UpdateOrder = (
|
||||
marketId: string,
|
||||
order: Partial<OrderObj>,
|
||||
persist?: boolean
|
||||
) => void;
|
||||
|
||||
interface Store {
|
||||
orders: OrderMap;
|
||||
update: UpdateOrder;
|
||||
}
|
||||
|
||||
export const STORAGE_KEY = 'vega_order_store';
|
||||
|
||||
export const useOrderStore = create<Store>()(
|
||||
persist(
|
||||
subscribeWithSelector((set) => ({
|
||||
orders: {},
|
||||
update: (marketId, order, persist = true) => {
|
||||
set((state) => {
|
||||
const curr = state.orders[marketId];
|
||||
const defaultOrder = getDefaultOrder(marketId);
|
||||
|
||||
return {
|
||||
orders: {
|
||||
...state.orders,
|
||||
[marketId]: {
|
||||
...defaultOrder,
|
||||
...curr,
|
||||
...order,
|
||||
persist,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: STORAGE_KEY,
|
||||
partialize: (state) => {
|
||||
// only store the order in localStorage if user has edited, this avoids
|
||||
// bloating localStorage if a user just visits the page but does not
|
||||
// edit the ticket
|
||||
const partializedOrders: OrderMap = {};
|
||||
for (const o in state.orders) {
|
||||
const order = state.orders[o];
|
||||
if (order && order.persist) {
|
||||
partializedOrders[order.marketId] = order;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
orders: partializedOrders,
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves an order from the store for a market and
|
||||
* creates one if it doesn't already exist
|
||||
*/
|
||||
export const useOrder = (marketId: string) => {
|
||||
const [order, _update] = useOrderStore((store) => {
|
||||
return [store.orders[marketId], store.update];
|
||||
});
|
||||
|
||||
const update = useCallback(
|
||||
(o: Partial<OrderObj>, persist = true) => {
|
||||
_update(marketId, o, persist);
|
||||
},
|
||||
[marketId, _update]
|
||||
);
|
||||
|
||||
// add new order to store if it doesnt exist, but don't
|
||||
// persist until user has edited
|
||||
useEffect(() => {
|
||||
if (!order) {
|
||||
update(
|
||||
getDefaultOrder(marketId),
|
||||
false // dont persist the order
|
||||
);
|
||||
}
|
||||
}, [order, marketId, update]);
|
||||
|
||||
return [order, update] as const; // make result a tuple
|
||||
};
|
||||
|
||||
export const getDefaultOrder = (marketId: string): OrderObj => ({
|
||||
marketId,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
side: Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
size: '0',
|
||||
price: '0',
|
||||
expiresAt: undefined,
|
||||
persist: false,
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import produce from 'immer';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type OrderData = OrderSubmissionBody['orderSubmission'] | null;
|
||||
|
||||
type PersistedOrderStore = {
|
||||
orders: OrderData[];
|
||||
getOrder: (marketId: string) => OrderData | undefined;
|
||||
setOrder: (order: OrderData) => void;
|
||||
clear: () => void;
|
||||
updatePrice: (marketId: string, price: string) => void;
|
||||
};
|
||||
|
||||
export const usePersistedOrderStore = create<PersistedOrderStore>()(
|
||||
persist(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
orders: [],
|
||||
getOrder: (marketId: string) => {
|
||||
const current = get() as PersistedOrderStore;
|
||||
const persisted = current.orders.find((o) => o?.marketId === marketId);
|
||||
return persisted;
|
||||
},
|
||||
setOrder: (order: OrderData) => {
|
||||
set(
|
||||
produce((store: PersistedOrderStore) => {
|
||||
const persisted = store.orders.find(
|
||||
(o) => o?.marketId === order?.marketId
|
||||
);
|
||||
if (persisted) {
|
||||
if (!isEqual(persisted, order)) {
|
||||
Object.assign(persisted, order);
|
||||
} else {
|
||||
// NOOP
|
||||
}
|
||||
} else {
|
||||
store.orders.push(order);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
clear: () => set({ orders: [] }),
|
||||
updatePrice: (marketId: string, price: string) =>
|
||||
set(
|
||||
produce((store: PersistedOrderStore) => {
|
||||
const persisted = store.orders.find(
|
||||
(o) => o?.marketId === marketId
|
||||
);
|
||||
if (persisted) {
|
||||
persisted.price = price;
|
||||
}
|
||||
})
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: 'VEGA_DEAL_TICKET_ORDER_STORE',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const usePersistedOrderStoreSubscription = (
|
||||
marketId: string,
|
||||
onOrderChange: (order: NonNullable<OrderData>) => void
|
||||
) => {
|
||||
const selector = (state: PersistedOrderStore) =>
|
||||
state.orders.find((o) => o?.marketId === marketId);
|
||||
const action = (storedOrder: OrderData | undefined) => {
|
||||
if (storedOrder) {
|
||||
onOrderChange(storedOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = usePersistedOrderStore.subscribe(selector, action);
|
||||
useEffect(() => () => unsubscribe(), [unsubscribe]);
|
||||
};
|
||||
@@ -21,8 +21,7 @@
|
||||
"**/*.stories.js",
|
||||
"**/*.stories.jsx",
|
||||
"**/*.stories.tsx",
|
||||
"jest.config.ts",
|
||||
"__mocks__"
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { hexlify } from 'ethers/lib/utils';
|
||||
import abi from '../abis/claim_abi.json';
|
||||
import { asciiToHex } from '../utils';
|
||||
|
||||
export const UNSPENT_CODE = '0x0000000000000000000000000000000000000000';
|
||||
export const SPENT_CODE = '0x0000000000000000000000000000000000000001';
|
||||
@@ -71,7 +71,7 @@ export class Claim {
|
||||
tranche,
|
||||
expiry,
|
||||
},
|
||||
hexlify(country),
|
||||
asciiToHex(country),
|
||||
target,
|
||||
].filter(Boolean)
|
||||
);
|
||||
@@ -109,7 +109,9 @@ export class Claim {
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
async isCountryBlocked(country: string): Promise<boolean> {
|
||||
const isAllowed = await this.contract.allowed_countries(hexlify(country));
|
||||
const isAllowed = await this.contract.allowed_countries(
|
||||
asciiToHex(country)
|
||||
);
|
||||
return !isAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* From:
|
||||
* https://github.com/ChainSafe/web3.js/blob/436e77a8eaa061fbaa183a9f73ca590c2e1d7697/packages/web3-utils/src/index.js
|
||||
*/
|
||||
export const asciiToHex = (str: string) => {
|
||||
if (!str) return '0x00';
|
||||
|
||||
let hex = '';
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
const n = code.toString(16);
|
||||
hex += n.length < 2 ? '0' + n : n;
|
||||
}
|
||||
|
||||
return '0x' + hex;
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from './ascii-to-hex';
|
||||
export * from './prepend-0x';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MAX_TRADES, tradesWithMarketProvider } from './trades-data-provider';
|
||||
import { TradesTable } from './trades-table';
|
||||
import type { Trade, TradeEdge } from './trades-data-provider';
|
||||
import type { TradesQueryVariables } from './__generated__/Trades';
|
||||
import { useOrderStore } from '@vegaprotocol/orders';
|
||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
@@ -20,7 +20,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
const totalCountRef = useRef<number | undefined>(undefined);
|
||||
const newRows = useRef(0);
|
||||
const scrolledToTop = useRef(true);
|
||||
const updateOrder = useOrderStore((store) => store.update);
|
||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
||||
|
||||
const variables = useMemo<TradesQueryVariables>(
|
||||
() => ({ marketId, maxTrades: MAX_TRADES }),
|
||||
@@ -115,7 +115,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
onBodyScroll={onBodyScroll}
|
||||
onClick={(price?: string) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
updatePrice(marketId, price);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -89,7 +89,6 @@ export const Notification = ({
|
||||
onClick={buttonProps.action}
|
||||
className={classNames(buttonProps.className)}
|
||||
data-testid={buttonProps.dataTestId}
|
||||
type="button"
|
||||
>
|
||||
{buttonProps.text}
|
||||
</Button>
|
||||
|
||||
@@ -5,12 +5,11 @@ import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
import type {
|
||||
OrderAmendment,
|
||||
OrderSubmission,
|
||||
OrderAmendmentBody,
|
||||
OrderSubmissionBody,
|
||||
Transaction,
|
||||
Transfer,
|
||||
} from './connectors';
|
||||
import type { Exact } from 'type-fest';
|
||||
|
||||
/**
|
||||
* Creates an ID in the same way that core does on the backend. This way we
|
||||
@@ -29,11 +28,11 @@ export const encodeTransaction = (tx: Transaction): string => {
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeOrderSubmission = <T extends Exact<OrderSubmission, T>>(
|
||||
order: T,
|
||||
export const normalizeOrderSubmission = (
|
||||
order: OrderSubmissionBody['orderSubmission'],
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
): OrderSubmission => ({
|
||||
): OrderSubmissionBody['orderSubmission'] => ({
|
||||
...order,
|
||||
price:
|
||||
order.type === OrderType.TYPE_LIMIT && order.price
|
||||
@@ -46,12 +45,12 @@ export const normalizeOrderSubmission = <T extends Exact<OrderSubmission, T>>(
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
||||
export const normalizeOrderAmendment = (
|
||||
order: Pick<Order, 'id' | 'timeInForce' | 'size' | 'expiresAt'>,
|
||||
market: Pick<Market, 'id' | 'decimalPlaces' | 'positionDecimalPlaces'>,
|
||||
price: string,
|
||||
size: string
|
||||
): OrderAmendment => ({
|
||||
): OrderAmendmentBody['orderAmendment'] => ({
|
||||
orderId: order.id,
|
||||
marketId: market.id,
|
||||
price: removeDecimal(price, market.decimalPlaces),
|
||||
@@ -66,7 +65,7 @@ export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
|
||||
export const normalizeTransfer = (
|
||||
address: string,
|
||||
amount: string,
|
||||
asset: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEthWithdrawApprovalsManager } from './use-ethereum-withdraw-approvals-manager';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import type { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@@ -318,25 +318,5 @@ describe('useEthWithdrawApprovalsManager', () => {
|
||||
expect(update.mock.calls[0][1].message).toEqual(
|
||||
'You are on the wrong network'
|
||||
);
|
||||
mockChainId = 111111;
|
||||
});
|
||||
|
||||
it('catch ethereum errors', async () => {
|
||||
const transaction = createWithdrawTransaction();
|
||||
mockUseGetWithdrawThreshold.mockReturnValueOnce(() => {
|
||||
throw new Error('call revert exception');
|
||||
});
|
||||
|
||||
mockEthTransactionStoreState.mockReturnValue({ create });
|
||||
mockEthWithdrawApprovalsStoreState.mockReturnValue({
|
||||
transactions: [transaction],
|
||||
update,
|
||||
});
|
||||
render();
|
||||
await waitFor(() => {
|
||||
const lastCall = update.mock.calls.pop();
|
||||
expect(lastCall[1].status).toEqual(ApprovalStatus.Error);
|
||||
expect(lastCall[1].message).toEqual('Something went wrong');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
|
||||
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { localLoggerFactory } from '@vegaprotocol/utils';
|
||||
|
||||
import { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
@@ -129,16 +128,7 @@ export const useEthWithdrawApprovalsManager = () => {
|
||||
approval.signatures,
|
||||
]
|
||||
);
|
||||
})().catch((err) => {
|
||||
localLoggerFactory({ application: 'web3' }).error(
|
||||
'create withdrawal transaction',
|
||||
err
|
||||
);
|
||||
update(transaction.id, {
|
||||
status: ApprovalStatus.Error,
|
||||
message: t('Something went wrong'),
|
||||
});
|
||||
});
|
||||
})();
|
||||
}, [
|
||||
getThreshold,
|
||||
getDelay,
|
||||
|
||||
Reference in New Issue
Block a user