Compare commits

...
Author SHA1 Message Date
Madalina Raicu 0a12fb5862 fix(trading): positions tooltip wording - updated tests 2023-08-07 18:14:20 +01:00
Madalina Raicu 4392678921 fix(trading): positions tooltip wordingcd '/Users/madalinaraicu/Desktop/VEGA/frontend-monorepo' 2023-08-07 18:13:17 +01:00
Madalina Raicu ab1a2cb222 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into develop 2023-08-07 16:22:11 +01:00
Art cdd91c24f2 feat(markets): succession line (#4491) 2023-08-07 11:46:03 +00:00
Sam Keen e3eb13ca72 fix(explorer): footer visual tweaks (#4499) 2023-08-07 11:54:46 +01:00
Matthew Russell 3f0ebbf33d fix(trading): dont render get started if connected (#4490) 2023-08-07 11:35:55 +01:00
Matthew Russell 026aa3964b chore(trading): update pennant (#4493) 2023-08-07 11:06:49 +01:00
Madalina Raicu e1a1ae0fa1 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into develop 2023-08-07 10:16:03 +01:00
daro-maj aa4c5a4a57 test(trading): update cypress tests (#4496) 2023-08-07 10:28:13 +02:00
Matthew Russell 0874314d2a chore: add specs (#4497) 2023-08-07 08:41:25 +01:00
Matthew Russell a70a7bcc8e fix(trading): dont show connect wallet message in deal ticket (#4482) 2023-08-04 11:09:43 +01:00
Matthew Russell 4461a6a497 fix(trading): remove hover state from disabled view as button (#4487) 2023-08-04 11:08:48 +01:00
Matthew Russell 1041864ad8 chore(trading): remove unnecessary forwarding of grid refs (#4486) 2023-08-04 11:08:28 +01:00
Matthew Russell 015e0188ec chore: enable iceberg orders 2023-08-03 12:38:51 +01:00
Matthew Russell 228236c152 chore(deal-ticket): type dropdown icon (#4468) 2023-08-03 12:15:21 +01:00
Art cd6e906dc6 feat(trading): view as button in sidebar (#4470) 2023-08-03 12:14:35 +01:00
Maciek 8a47a92dbd fix(trading): add info splashes on deposit and withdrawals list containers (#4479) 2023-08-03 12:01:42 +01:00
Matthew Russell af14c27df9 chore(trading): responsive welcome dialog content and content update (#4475) 2023-08-03 12:01:09 +01:00
Matthew Russell 38edcd88c0 chore(trading): fix alignment of expire (#4477) 2023-08-03 12:00:12 +01:00
m.ray a74cf02030 feat(trading): order details dialog (#4281) 2023-08-03 10:05:12 +00:00
Joe Tsang 6686755763 chore(governance): fix failing tests from nightly run (#4453) 2023-08-03 10:01:00 +01:00
Joe Tsang 79cf8163be chore(governance): add e2e test for successor market (#4471) 2023-08-03 09:54:51 +01:00
Matthew Russell 1688e26f4b feat(trading): add fairground texture background to logo (#4474) 2023-08-03 09:28:01 +01:00
Art 5e101a8eaf feat(trading): deposit page (#4472) 2023-08-02 19:06:49 +02:00
120 changed files with 5828 additions and 2004 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
---
name: Feature Epic
about:
A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'Epic: '
labels: feature-epic
---
@@ -23,7 +23,7 @@ export const Footer = () => {
);
return (
<footer className="grid grid-rows-2 lg:grid-cols-[1fr_auto] text-xs md:text-md lg:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<footer className="grid grid-cols-[1fr_auto] items-center text-xs md:text-md lg:flex md:col-span-2 px-4 pt-2 pb-3 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
@@ -43,9 +43,12 @@ export const Footer = () => {
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
<div className="content-center flex pr-4 md:border-r border-neutral-700 dark:border-neutral-300">
<span className="pr-2">{VEGA_URL && <NodeUrl url={VEGA_URL} />}</span>
<Link
className="ml-2 underline-offset-4"
onClick={() => setNodeSwitcherOpen(true)}
>
{t('Change')}
</Link>
</div>
@@ -59,7 +62,10 @@ export const Footer = () => {
) : null}
</div>
<div className="pl-2 align-center lg:align-right lg:flex lg:justify-end gap-2 align-middle lg:max-w-xs lg:ml-auto">
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline">
<RouteLink
to={`/${Routes.DISCLAIMER}`}
className="underline underline-offset-4"
>
Disclaimer
</RouteLink>
</div>
@@ -0,0 +1,107 @@
{
"changes": {
"decimalPlaces": "5",
"positionDecimalPlaces": "5",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"lpPriceRange": "10",
"instrument": {
"name": "Token test market",
"code": "Token.24h",
"future": {
"settlementAsset": "816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc",
"quoteName": "fUSDC",
"dataSourceSpecForSettlementData": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.BTC.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
}
]
}
]
}
}
},
"dataSourceSpecForTradingTermination": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
}
]
}
]
}
}
},
"dataSourceSpecBinding": {
"settlementDataProperty": "prices.BTC.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
},
"metadata": ["sector:food", "sector:materials", "source:docs.vega.xyz"],
"priceMonitoringParameters": {
"triggers": [
{
"horizon": "43200",
"probability": "0.9999999",
"auctionExtension": "600"
}
]
},
"liquidityMonitoringParameters": {
"targetStakeParameters": {
"timeWindow": "3600",
"scalingFactor": 10
},
"triggeringRatio": "0.7",
"auctionExtension": "1"
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
"params": {
"mu": 0,
"r": 0.016,
"sigma": 0.8
}
},
"successor": {
"parentMarketId": "",
"insurancePoolFraction": "0.75"
}
}
}
@@ -220,7 +220,7 @@ context(
function () {
const proposalTitle = 'Test new market proposal';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type('Test new market proposal');
cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
@@ -606,6 +606,94 @@ context(
});
});
it('able to submit successor market proposal', function () {
const proposalTitle = 'Test successor market proposal';
cy.createMarket();
cy.reload();
waitForSpinner();
cy.getByTestId('closed-proposals').within(() => {
cy.contains('Add Lorem Ipsum market')
.parentsUntil(proposalListItem)
.last()
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
});
getProposalInformationFromTable('ID').invoke('text').as('parentMarketId');
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type(
'E2E test for successor market'
);
cy.fixture('/proposals/successor-market').then((newMarketProposal) => {
newMarketProposal.changes.successor.parentMarketId =
this.parentMarketId;
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.getByTestId(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.getByTestId(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath });
});
});
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() => {
// 3003-PMAN-008
cy.getByTestId('proposal-successor-info')
.should('have.text', 'Successor market to: TEST.24h')
.find('a')
.should('have.attr', 'href')
.and('contain', this.parentMarketId);
cy.getByTestId('view-proposal-btn').click();
});
// #3003-PMAN-010
cy.getByTestId(proposalJsonToggle).click();
cy.get('.language-json').within(() => {
cy.get('.hljs-attr').should('contain.text', 'parentMarketId');
cy.get('.hljs-string').should('contain.text', this.parentMarketId);
cy.get('.hljs-attr').should('contain.text', 'insurancePoolFraction');
cy.get('.hljs-string').should('contain.text', '0.75');
});
cy.getByTestId('proposal-market-data').within(() => {
cy.getByTestId('proposal-market-data-toggle').click();
cy.contains('Key details').click();
// 3003-PMAN-009
getMarketProposalDetailsFromTable('Parent Market ID').should(
'have.text',
this.parentMarketId
);
getMarketProposalDetailsFromTable('Insurance Pool Fraction').should(
'have.text',
'0.75'
);
getMarketProposalDetailsFromTable('Trading Mode').should(
'have.text',
'No trading'
);
});
// 3003-PMAN-011
cy.contains('Parent Market ID').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The ID of the market this market succeeds.'
);
cy.contains('Insurance Pool Fraction').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The fraction of the insurance pool balance that is carried over from the parent market to the successor.'
);
});
after('Disassociate from second wallet key if present', function () {
cy.reload();
waitForSpinner();
@@ -96,12 +96,14 @@ context('rewards - flow', { tags: '@slow' }, function () {
.within(() => {
cy.get('h2').first().should('contain.text', 'EPOCH');
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut)
.should('contain.text', '0.4415')
.and('contain.text', '(44.15%)');
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')
.should('contain.text', '0.0004')
.and('contain.text', '(44.15%)');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut).should(
'contain.text',
'0.4415'
);
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
'contain.text',
'0.0004'
);
cy.getByTestId('total').should('have.text', '0.4419');
});
});
@@ -339,7 +339,7 @@ context(
cy.getByTestId(vegaWalletCurrencyTitle)
.contains(name)
.parent()
.siblings()
.siblings(txTimeout)
.should((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount);
@@ -107,7 +107,7 @@ export function dissociateFromSecondWalletKey() {
cy.getByTestId('vega-in-wallet')
.first()
.within(() => {
cy.getByTestId('eth-wallet-associated-balances')
cy.getByTestId('eth-wallet-associated-balances', txTimeout)
.last()
.within(() => {
cy.getByTestId('associated-key')
@@ -78,13 +78,21 @@ export function stakingPageAssociateTokens(
}
cy.get(tokenAmountInputBox, epochTimeout).type(amount);
if (approve) {
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
cy.contains('Approve $VEGA Tokens for staking on Vega').should(
'be.visible'
);
cy.contains('Approve $VEGA Tokens for staking on Vega', txTimeout).should(
'not.exist'
);
cy.getByTestId('wallet-associate').then((walletAssociateField) => {
if (
walletAssociateField.find('[data-testid="token-input-approve-button"]')
.length
) {
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
cy.contains('Approve $VEGA Tokens for staking on Vega').should(
'be.visible'
);
cy.contains(
'Approve $VEGA Tokens for staking on Vega',
txTimeout
).should('not.exist');
}
});
}
cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click();
+1
View File
@@ -26,6 +26,7 @@ module.exports = defineConfig({
requestTimeout: 20000,
retries: 1,
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
@@ -5,12 +5,11 @@ describe('charts', { tags: '@smoke' }, () => {
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
});
it('can see market depth chart', () => {
// 6006-DEPC-001
cy.getByTestId('Depth').click();
cy.getByTestId('tab-depth').should('be.visible');
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
});
});
@@ -155,7 +155,10 @@ describe(
{ name: 'Volume', infoText: 'Volume: 55,000' },
];
cy.get(indicatorInfo).eq(1).realHover();
cy.get('.close-button-module_closeButton__2ifkl').click({ force: true });
cy.get('.chart__wrapper [data-testid="split-view-view"]')
.last()
.find('[role="button"][title="Close"]')
.click({ force: true });
cy.get(indicatorInfo).should('have.length', 1);
checkMenuItemCheckbox('Studies', studyInfo);
@@ -22,9 +22,8 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
cy.getByTestId('get-started-button').should('exist');
});
it('must be able to select order direction - long/short', function () {
@@ -45,7 +44,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('get-started-button').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
@@ -251,11 +251,13 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]').within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
cy.get('[col-id="openVolume"]')
.eq(3)
.within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
@@ -0,0 +1,111 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const manageVegaWallet = 'manage-vega-wallet';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
@@ -4,118 +4,16 @@ const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
@@ -132,6 +30,8 @@ describe(
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
+1 -1
View File
@@ -18,5 +18,5 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
@@ -0,0 +1,19 @@
import { DepositContainer } from '@vegaprotocol/deposits';
import { GetStarted } from '../../components/welcome-dialog';
import { t } from '@vegaprotocol/i18n';
export const Deposit = () => {
return (
<div className="py-16 px-8 flex w-full justify-center">
<div className="lg:min-w-[700px] min-w-[300px] max-w-[700px]">
<h1 className="text-4xl xl:text-5xl uppercase font-alpha calt">
{t('Deposit')}
</h1>
<div className="mt-10">
<DepositContainer />
<GetStarted />
</div>
</div>
</div>
);
};
@@ -0,0 +1,3 @@
import { Deposit } from './deposit';
export default Deposit;
@@ -1,15 +1,12 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useSidebar, ViewType } from '../../components/sidebar';
export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet();
const { data, error } = useDataProvider({
dataProvider: depositsProvider,
@@ -17,11 +14,13 @@ export const DepositsContainer = () => {
skip: !pubKey,
});
const setView = useSidebar((store) => store.setView);
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<div className="h-full">
<DepositsTable
rowData={data}
ref={gridRef}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
/>
{!isReadOnly && (
@@ -1,4 +1,4 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import {
withdrawalProvider,
WithdrawalsTable,
@@ -18,7 +18,9 @@ export const WithdrawalsContainer = () => {
});
const setView = useSidebar((store) => store.setView);
const { ready, delayed } = useIncompleteWithdrawals();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<>
<div className="h-full relative">
@@ -52,6 +52,9 @@ export const Navbar = ({
'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
'text-default': theme === 'system',
})}
style={{
background: theme === 'yellow' ? 'url(/testnet-logo-bg.png' : 'none',
}}
>
<VLogo className="w-4" />
</NavLink>
@@ -1,7 +1,15 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Sidebar, SidebarContent, ViewType, useSidebar } from './sidebar';
import {
Sidebar,
SidebarButton,
SidebarContent,
ViewType,
useSidebar,
} from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />,
@@ -28,11 +36,14 @@ describe('Sidebar', () => {
'does not render ticket and info',
(path) => {
render(
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -47,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -65,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
const settingsButton = screen.getByTestId(ViewType.Settings);
@@ -91,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => {
it('renders the correct content', () => {
const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
expect(container).toBeEmptyDOMElement();
@@ -115,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => {
const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
act(() => {
@@ -141,3 +161,25 @@ describe('SidebarContent', () => {
expect(container).toBeEmptyDOMElement();
});
});
describe('SidebarButton', () => {
it.each([ViewType.Info, ViewType.Deposit, ViewType.ViewAs])(
'runs given callback regardless of requested view (%s)',
async (view) => {
const onClick = jest.fn();
render(
<SidebarButton
icon={VegaIconNames.INFO}
tooltip="INFO"
onClick={onClick}
view={view}
/>
);
const btn = screen.getByTestId(view);
await userEvent.click(btn);
expect(onClick).toBeCalled();
}
);
});
+43 -16
View File
@@ -16,6 +16,7 @@ import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store';
@@ -26,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw',
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
}
type SidebarView =
@@ -53,6 +55,8 @@ type SidebarView =
export const Sidebar = () => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
return (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}>
@@ -105,6 +109,16 @@ export const Sidebar = () => {
</Routes>
</nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
@@ -116,25 +130,43 @@ export const Sidebar = () => {
);
};
const SidebarButton = ({
export const SidebarButton = ({
view,
icon,
tooltip,
disabled = false,
onClick,
}: {
view: ViewType;
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => {
const { currView, setView } = useSidebar((store) => ({
currView: store.view,
setView: store.setView,
}));
const buttonClasses = classNames('flex items-center p-1 rounded', {
'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500':
view !== currView?.type,
'bg-vega-yellow hover:bg-vega-yellow-550 text-black':
view === currView?.type,
});
const onSelect = (view: SidebarView['type']) => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip
description={tooltip}
@@ -146,13 +178,8 @@ const SidebarButton = ({
<button
className={buttonClasses}
data-testid={view}
onClick={() => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
}}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
</button>
@@ -282,7 +309,7 @@ export const useSidebar = create<{
view: null,
setView: (x) =>
set(() => {
if (x === null) {
if (x == null) {
return { view: null, init: false };
}
@@ -0,0 +1,31 @@
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { GetStarted } from './get-started';
import { render, screen } from '@testing-library/react';
describe('GetStarted', () => {
const renderComponent = (context: Partial<VegaWalletContextShape> = {}) => {
return render(
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
);
};
it('renders full get started content if not connected and no browser wallet detected', () => {
renderComponent();
expect(screen.getByTestId('get-started-banner')).toBeInTheDocument();
});
it('renders connect prompt if no pubKey but wallet installed', () => {
globalThis.window.vega = {} as Vega;
renderComponent();
expect(screen.getByTestId('order-connect-wallet')).toBeInTheDocument();
globalThis.window.vega = undefined as unknown as Vega;
});
it('renders nothing if connected', () => {
const { container } = renderComponent({ pubKey: 'my-pubkey' });
expect(container).toBeEmptyDOMElement();
});
});
@@ -16,7 +16,6 @@ interface Props {
export const GetStarted = ({ lead }: Props) => {
const { pubKey } = useVegaWallet();
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
@@ -33,18 +32,18 @@ export const GetStarted = ({ lead }: Props) => {
setOnboardingViewed('true');
};
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
'bg-vega-blue-300 dark:bg-vega-blue-700',
'border border-vega-blue-350 dark:border-vega-blue-650',
{ 'mt-8': !lead }
);
if (!pubKey && !isBrowserWalletInstalled()) {
return (
<div
className={classNames(
'flex flex-col bg-vega-blue-300 dark:bg-vega-blue-700 border border-vega-blue-350 dark:border-vega-blue-650 px-6 gap-4',
{ 'py-4': lead },
{ 'mt-8 py-6': !lead }
)}
data-testid="get-started-banner"
>
<div className={wrapperClasses} data-testid="get-started-banner">
{lead && <h2>{lead}</h2>}
<h3>{t('Get started')}</h3>
<h3 className="text-lg">{t('Get started')}</h3>
<div>
<ul className="list-decimal list-inside">
<li>{t('Get a Vega wallet')}</li>
@@ -57,7 +56,6 @@ export const GetStarted = ({ lead }: Props) => {
<TradingButton
intent={Intent.Info}
onClick={onButtonClick}
className="block w-full"
data-testid="get-started-button"
>
{t('Get started')}
@@ -82,5 +80,28 @@ export const GetStarted = ({ lead }: Props) => {
</div>
);
}
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="text-sm mb-1">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
to start trading in this market.
</p>
<TradingButton
onClick={openVegaWalletDialog}
size="small"
data-testid="order-connect-wallet"
intent={Intent.Info}
>
{t('Connect wallet')}
</TradingButton>
</div>
);
}
return null;
};
@@ -6,6 +6,7 @@ import { Links, Routes } from '../../pages/client-router';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import * as constants from '../constants';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
@@ -25,206 +26,134 @@ export const WelcomeDialogContent = () => {
'Free from the risks of real trading, Fairground is a safe and fun place to try out Vega yourself with virtual assets.'
);
return (
<div className="flex flex-col pb-2">
<div className="flex gap-8">
<div className="w-1/2 flex flex-col justify-between pt-3">
<ul className="ml-0">
<li className="my-3 flex gap-3 text-default">
<div className="shrink-0 pt-1.5">
<svg
width="40"
height="40"
viewBox="0 0 30 30"
fill="currentColor"
>
<rect x="14" y="20" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="10" y="8" width="2" height="10" />
<rect x="16" y="18" width="2" height="2" />
<rect x="20" y="16" width="2" height="2" />
<rect x="18" y="8" width="2" height="8" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect
x="28"
width="2"
height="26"
transform="rotate(90 28 0)"
/>
<rect
x="28"
y="28"
width="2"
height="26"
transform="rotate(90 28 28)"
/>
</svg>
</div>
<div>
<h3 className="text-lg">{t('Trade with no KYC')}</h3>
<span className="text-sm text-vega-clight-100 dark:text-vega-cdark-100 leading-4">
{t(
'Pseudonomously trade Futures markets. Spot and perps comming soon'
)}
</span>
</div>
</li>
<li className="my-3 flex gap-3 text-vega-clight-50 dark:text-vega-cdark-50">
<div className="shrink-0 pt-1.5">
<svg
width="40"
height="40"
viewBox="0 0 30 30"
fill="currentColor"
>
<g clip-path="url(#clip0_5168_49089)">
<path d="M22 7H18V9H22V7Z" />
<path d="M22 13H18V15H22V13Z" />
<path d="M24 17H16V19H24V17Z" />
<path d="M24 9H22V13H24V9Z" />
<path d="M18 9H16V13H18V9Z" />
<path d="M6 19H4V23H6V19Z" />
<path d="M16 19H14V23H16V19Z" />
<path d="M26 19H24V23H26V19Z" />
<path d="M12 7H8V9H12V7Z" />
<path d="M12 13H8V15H12V13Z" />
<path d="M14 17H6V19H14V17Z" />
<path d="M14 9H12V13H14V9Z" />
<path d="M8 9H6V13H8V9Z" />
<path d="M30 2H28V28H30V2Z" />
<path d="M2 2H0V28H2V2Z" />
<path d="M28 0H2V2H28V0Z" />
<path d="M28 28H2V30H28V28Z" />
</g>
<defs>
<clipPath id="clip0_5168_49089">
<rect width="30" height="30" />
</clipPath>
</defs>
</svg>
</div>
<div>
<h3 className="text-lg">
{t('Community generated trading pairs')}
</h3>
<span className="text-sm text-vega-clight-100 dark:text-vega-cdark-100 leading-4">
{t('All markets are proposed and enacted by the community')}
</span>
</div>
</li>
<li className="my-3 flex gap-3 text-vega-clight-50 dark:text-vega-cdark-50">
<div className="shrink-0 pt-1.5">
<svg
width="40"
height="40"
viewBox="0 0 30 30"
fill="currentColor"
>
<rect x="6" y="16" width="4" height="2" />
<rect x="10" y="14" width="2" height="2" />
<rect
x="12"
y="24"
width="4"
height="2"
transform="rotate(-90 12 24)"
/>
<rect
x="10"
y="20"
width="2"
height="2"
transform="rotate(-90 10 20)"
/>
<rect
x="20"
y="18"
width="4"
height="2"
transform="rotate(-180 20 18)"
/>
<rect
x="16"
y="20"
width="2"
height="2"
transform="rotate(-180 16 20)"
/>
<rect
x="16"
y="14"
width="2"
height="2"
transform="rotate(90 16 14)"
/>
<rect
x="22"
y="10"
width="2"
height="2"
transform="rotate(90 22 10)"
/>
<rect
x="20"
y="8"
width="2"
height="2"
transform="rotate(90 20 8)"
/>
<rect
x="24"
y="8"
width="2"
height="2"
transform="rotate(90 24 8)"
/>
<rect
x="22"
y="6"
width="2"
height="2"
transform="rotate(90 22 6)"
/>
<rect x="12" y="10" width="2" height="4" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect
x="28"
width="2"
height="26"
transform="rotate(90 28 0)"
/>
<rect
x="28"
y="28"
width="2"
height="26"
transform="rotate(90 28 28)"
/>
</svg>
</div>
<div>
<h3 className="text-lg">{t('Rewards')}</h3>
<span className="text-sm text-vega-clight-100 dark:text-vega-cdark-100 leading-4">
{t(
'Earn rewards for trading, market making and providing liquidity'
)}
</span>
</div>
</li>
</ul>
<TradingButton
onClick={browseMarkets}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
</TradingButton>
</div>
<div className="w-1/2 -mr-3 flex flex-grow">
<GetStarted lead={lead} />
</div>
<div className="flex flex-col sm:flex-row gap-8">
<div className="sm:w-1/2 flex flex-col justify-between pt-3">
<ul className="ml-0">
<ListItemContent
icon={<NonCustodialIcon />}
title={t('Non-custodial and pseudonymous')}
text={t('No third party has access to your funds.')}
/>
<ListItemContent
icon={<PurposeBuiltIcon />}
title={t('Purpose built proof of stake blockchain')}
text={t(
'Fully decentralised high performance peer-to-network trading.'
)}
/>
<ListItemContent
icon={<RewardIcon />}
title={t('Low fees and no cost to place orders')}
text={t(
'Fees work like a CEX with no per-transaction gas for orders'
)}
/>
</ul>
<TradingButton
onClick={browseMarkets}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
</TradingButton>
</div>
<div className="sm:w-1/2">
<GetStarted lead={lead} />
</div>
</div>
);
};
const ListItemContent = ({
icon,
title,
text,
}: {
icon: ReactNode;
title: string;
text: string;
}) => {
return (
<li className="my-4 flex gap-3">
<div className="shrink-0 pt-1">{icon}</div>
<div>
<h3 className="text-lg leading-snug mb-2">{title}</h3>
<p className="text-sm text-secondary">{text}</p>
</div>
</li>
);
};
const PurposeBuiltIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="14" y="20" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="10" y="8" width="2" height="10" />
<rect x="16" y="18" width="2" height="2" />
<rect x="20" y="16" width="2" height="2" />
<rect x="18" y="8" width="2" height="8" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const NonCustodialIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" className="fill-current">
<rect x="8" y="22" width="14" height="2" />
<rect x="8" y="12" width="14" height="2" />
<rect
x="22"
y="24"
width="2"
height="12"
transform="rotate(-180 22 24)"
/>
<rect
x="10"
y="24"
width="2"
height="12"
transform="rotate(-180 10 24)"
/>
<rect x="12" y="12" width="2" height="2" transform="rotate(-180 12 12)" />
<rect x="20" y="12" width="2" height="2" transform="rotate(-180 20 12)" />
<rect x="12" y="10" width="2" height="2" transform="rotate(-180 12 10)" />
<rect x="20" y="10" width="2" height="2" transform="rotate(-180 20 10)" />
<rect x="18" y="8" width="2" height="2" transform="rotate(-180 18 8)" />
<rect x="14" y="8" width="2" height="2" transform="rotate(-180 14 8)" />
<rect x="16" y="8" width="2" height="2" transform="rotate(-180 16 8)" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const RewardIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="6" y="16" width="4" height="2" />
<rect x="10" y="14" width="2" height="2" />
<rect x="12" y="24" width="4" height="2" transform="rotate(-90 12 24)" />
<rect x="10" y="20" width="2" height="2" transform="rotate(-90 10 20)" />
<rect x="20" y="18" width="4" height="2" transform="rotate(-180 20 18)" />
<rect x="16" y="20" width="2" height="2" transform="rotate(-180 16 20)" />
<rect x="16" y="14" width="2" height="2" transform="rotate(90 16 14)" />
<rect x="22" y="10" width="2" height="2" transform="rotate(90 22 10)" />
<rect x="20" y="8" width="2" height="2" transform="rotate(90 20 8)" />
<rect x="24" y="8" width="2" height="2" transform="rotate(90 24 8)" />
<rect x="22" y="6" width="2" height="2" transform="rotate(90 22 6)" />
<rect x="12" y="10" width="2" height="4" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
+7
View File
@@ -31,6 +31,10 @@ const LazyDisclaimer = dynamic(() => import('../client-pages/disclaimer'), {
ssr: false,
});
const LazyDeposit = dynamic(() => import('../client-pages/deposit'), {
ssr: false,
});
export enum Routes {
HOME = '/',
MARKET = '/markets/:marketId',
@@ -38,6 +42,7 @@ export enum Routes {
PORTFOLIO = '/portfolio',
LIQUIDITY = '/liquidity/:marketId',
DISCLAIMER = '/disclaimer',
DEPOSIT = '/deposit',
}
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -51,6 +56,7 @@ export const Links: ConsoleLinks = {
[Routes.LIQUIDITY]: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
[Routes.DISCLAIMER]: () => Routes.DISCLAIMER,
[Routes.DEPOSIT]: () => Routes.DEPOSIT,
};
const routerConfig: RouteObject[] = [
@@ -97,6 +103,7 @@ const routerConfig: RouteObject[] = [
path: Routes.DISCLAIMER,
element: <LazyDisclaimer />,
},
{ path: Routes.DEPOSIT, element: <LazyDeposit /> },
{
path: '*',
element: (
+2 -1
View File
@@ -2,7 +2,7 @@ import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import {
Web3ConnectUncontrolledDialog,
@@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors}
riskMessage={<RiskMessage />}
/>
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -118,7 +118,6 @@ export const AccountManager = ({
onMarketClick,
gridProps,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
const { data, error } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider,
@@ -138,7 +137,6 @@ export const AccountManager = ({
return (
<div className="relative h-full">
<AccountTable
ref={gridRef}
rowData={data}
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
+237 -243
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo, useCallback } from 'react';
import { useMemo, useCallback } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
@@ -21,7 +21,7 @@ import type {
RowHeightParams,
ColDef,
} from 'ag-grid-community';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import type { Asset } from '@vegaprotocol/types';
import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid';
@@ -75,256 +75,250 @@ export interface AccountTableProps extends AgGridReactProps {
pinnedAsset?: PinnedAsset;
}
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
(
{
onClickAsset,
onClickWithdraw,
onClickDeposit,
onClickBreakdown,
onClickTransfer,
rowData,
isReadOnly,
pinnedAsset,
...props
export const AccountTable = ({
onClickAsset,
onClickWithdraw,
onClickDeposit,
onClickBreakdown,
onClickTransfer,
rowData,
isReadOnly,
pinnedAsset,
...props
}: AccountTableProps) => {
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props;
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
},
ref
) => {
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
[pinnedAsset?.id, getRowHeight]
);
const { getRowHeight } = props;
const showDepositButton = pinnedRow?.balance === '0';
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
},
[pinnedAsset?.id, getRowHeight]
);
const showDepositButton = pinnedRow?.balance === '0';
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? (
<>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-1 inline-block w-14'
)}
return data.breakdown ? (
<>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-1 inline-block w-14'
)}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
);
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
return (
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
ref={ref}
tooltipShowDelay={500}
rowData={data}
defaultColDef={{
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
comparator: accountValuesComparator,
}}
columnDefs={colDefs}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
/>
);
}
);
return (
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
tooltipShowDelay={500}
rowData={data}
defaultColDef={{
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
comparator: accountValuesComparator,
}}
columnDefs={colDefs}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
/>
);
};
+17 -1
View File
@@ -4,9 +4,25 @@ export default {
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
'^.+\\.[tj]sx?$': [
'babel-jest',
{
presets: ['@nx/react/babel'],
// required for pennant to work in jest, due to having untranspiled exports
plugins: [
[
'@babel/plugin-proposal-private-methods',
{
loose: true,
},
],
],
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/candles-chart',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
// dont ignore pennant from transpilation
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
};
-1
View File
@@ -21,5 +21,4 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar';
export * from './lib/ag-grid-update';
export * from './lib/use-datagrid-events';
-20
View File
@@ -1,20 +0,0 @@
import type { MutableRefObject, RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
type AnyArray = Array<any> | null; // eslint-disable-line @typescript-eslint/no-explicit-any
export const isXOrWasEmpty = (prev?: AnyArray, curr?: AnyArray) =>
Boolean(Number(!!prev?.length) ^ Number(!!curr?.length));
export const updateGridData = (
dataRef: MutableRefObject<AnyArray>,
data: AnyArray,
gridRef: RefObject<AgGridReact>
) => {
const rerender = isXOrWasEmpty(dataRef.current, data);
dataRef.current = data;
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
gridRef.current?.api?.refreshInfiniteCache();
}
return !rerender;
};
@@ -158,16 +158,13 @@ describe('StopOrder', () => {
);
});
it('shows no wallet warning and do not submit if no wallet connected', async () => {
it('does not submit if no wallet connected', async () => {
render(generateJsx(null));
await userEvent.type(screen.getByTestId(sizeInput), '1');
await userEvent.type(screen.getByTestId(priceInput), '1');
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
await userEvent.click(screen.getByTestId(submitButton));
expect(submit).not.toBeCalled();
expect(
screen.getByTestId('deal-ticket-connect-wallet')
).toBeInTheDocument();
});
it('calls submit if form is valid', async () => {
@@ -488,19 +488,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</InputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-end">
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div>
<div className="mb-2">
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="expire"
control={control}
@@ -511,11 +499,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={'Expire'}
label={<span className="text-xs">{t('Expire')}</span>}
/>
);
}}
/>
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div>
{expire && (
<>
@@ -570,7 +568,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</div>
</>
)}
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
<NoWalletWarning isReadOnly={isReadOnly} />
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
@@ -13,14 +13,9 @@ 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 { normalizeOrderSubmission, useVegaWallet } from '@vegaprotocol/wallet';
import {
Checkbox,
ExternalLink,
InputError,
Intent,
Notification,
@@ -637,13 +632,7 @@ interface SummaryMessageProps {
export const NoWalletWarning = ({
isReadOnly,
pubKey,
asset,
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
const assetSymbol = asset.symbol;
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
}: Pick<SummaryMessageProps, 'isReadOnly'>) => {
if (isReadOnly) {
return (
<div className="mb-2">
@@ -655,31 +644,6 @@ export const NoWalletWarning = ({
</div>
);
}
if (!pubKey) {
return (
<div className="mb-2">
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'small',
}}
/>
</div>
);
}
return null;
};
@@ -698,13 +662,7 @@ const SummaryMessage = memo(
// Specific error UI for if balance is so we can
// render a deposit dialog
if (isReadOnly || !pubKey) {
return (
<NoWalletWarning
isReadOnly={isReadOnly}
asset={asset}
pubKey={pubKey}
/>
);
return <NoWalletWarning isReadOnly={isReadOnly} />;
}
if (errorMessage === SummaryValidationType.NoCollateral) {
@@ -1,5 +1,4 @@
import {
Icon,
InputError,
SimpleGrid,
Tooltip,
@@ -10,6 +9,8 @@ import {
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
@@ -81,11 +82,11 @@ export const TypeToggle = ({
}
)}
>
<button>
<button className="flex gap-1">
<span className="text-ellipsis whitespace-nowrap shrink overflow-hidden">
{t(selectedOption ? selectedOption.label : 'Stop')}
</span>
<Icon name="chevron-down" className="ml-1" />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
</button>
</TradingDropdownTrigger>
}
+5 -8
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo } from 'react';
import { useMemo } from 'react';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
@@ -6,7 +6,6 @@ import {
isNumeric,
} from '@vegaprotocol/utils';
import type { ColDef } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
VegaICellRendererParams,
@@ -17,10 +16,9 @@ import type { DepositFieldsFragment } from './__generated__/Deposit';
import { EtherscanLink } from '@vegaprotocol/environment';
import { DepositStatusMapping } from '@vegaprotocol/types';
export const DepositsTable = forwardRef<
AgGridReact,
TypedDataAgGrid<DepositFieldsFragment>
>((props, ref) => {
export const DepositsTable = (
props: TypedDataAgGrid<DepositFieldsFragment>
) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{ headerName: 'Asset', field: 'asset.symbol' },
@@ -79,11 +77,10 @@ export const DepositsTable = forwardRef<
);
return (
<AgGrid
ref={ref}
defaultColDef={{ flex: 1 }}
columnDefs={columnDefs}
style={{ width: '100%', height: '100%' }}
{...props}
/>
);
});
};
+1 -4
View File
@@ -1,8 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
@@ -32,7 +31,6 @@ export const LedgerManager = ({
partyId: string;
gridProps: ReturnType<typeof useDataGridEvents>;
}) => {
const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>(
@@ -64,7 +62,6 @@ export const LedgerManager = ({
return (
<div className="h-full relative">
<LedgerTable
ref={gridRef}
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
{...gridProps}
+150 -154
View File
@@ -14,7 +14,6 @@ import {
DateRangeFilter,
SetFilter,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import {
@@ -23,7 +22,7 @@ import {
TransferTypeMapping,
} from '@vegaprotocol/types';
import type { LedgerEntry } from './ledger-entries-data-provider';
import { forwardRef, useMemo } from 'react';
import { useMemo } from 'react';
import { formatRFC3339, subDays } from 'date-fns';
export const TransferTooltipCellComponent = ({
@@ -45,162 +44,159 @@ const dateRangeFilterParams = {
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
(props, ref) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
export const LedgerTable = (props: LedgerEntryProps) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
flex: 1,
},
],
[]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
flex: 1,
},
],
[]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
}}
columnDefs={columnDefs}
{...props}
/>
);
}
);
}}
columnDefs={columnDefs}
{...props}
/>
);
};
+17 -1
View File
@@ -4,9 +4,25 @@ export default {
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
'^.+\\.[tj]sx?$': [
'babel-jest',
{
presets: ['@nx/react/babel'],
// required for pennant to work in jest, due to having untranspiled exports
plugins: [
[
'@babel/plugin-proposal-private-methods',
{
loose: true,
},
],
],
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/market-depth',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
// dont ignore pennant from transpilation
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
};
@@ -34,5 +34,8 @@ query SuccessorMarket($marketId: ID!) {
code
}
}
proposal {
id
}
}
}
+4 -1
View File
@@ -27,7 +27,7 @@ export type SuccessorMarketQueryVariables = Types.Exact<{
}>;
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } | null };
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } }, proposal?: { __typename?: 'Proposal', id?: string | null } | null } | null };
export const SuccessorMarketIdDocument = gql`
@@ -153,6 +153,9 @@ export const SuccessorMarketDocument = gql`
code
}
}
proposal {
id
}
}
}
`;
@@ -1,4 +1,8 @@
import { TokenStaticLinks, useEnvironment } from '@vegaprotocol/environment';
import {
FLAGS,
TokenStaticLinks,
useEnvironment,
} from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -34,6 +38,7 @@ import {
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
SuccessionLineInfoPanel,
} from './market-info-panels';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
@@ -291,6 +296,13 @@ export const MarketInfoAccordion = ({
</>
}
/>
{FLAGS.SUCCESSOR_MARKETS && (
<AccordionItem
itemId="succession-line"
title={t('Succession line')}
content={<SuccessionLineInfoPanel market={market} />}
/>
)}
</Accordion>
</div>
)}
@@ -1,9 +1,11 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import {
ConditionOperator,
ConditionOperatorMapping,
} from '@vegaprotocol/types';
import { DataSourceProof } from './market-info-panels';
import { DataSourceProof, SuccessionLineInfoPanel } from './market-info-panels';
import { MockedProvider } from '@apollo/react-testing';
import { SuccessorMarketIdsDocument } from '../../__generated__';
jest.mock('../../hooks/use-oracle-markets', () => ({
useOracleMarkets: () => [],
@@ -137,3 +139,93 @@ describe('DataSourceProof', () => {
).toBeInTheDocument();
});
});
describe('SuccessionLineInfoPanel', () => {
const mocks = [
{
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
__typename: 'Query',
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'abc',
successorMarketID: 'def',
parentMarketID: null,
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'def',
successorMarketID: 'ghi',
parentMarketID: 'abc',
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'ghi',
successorMarketID: null,
parentMarketID: 'def',
},
},
],
},
},
},
},
];
it.each([
['abc', 1],
['def', 2],
['ghi', 3],
])(
'renders succession line for %s (current position %d)',
async (id, number) => {
render(
<MockedProvider mocks={mocks}>
<SuccessionLineInfoPanel
market={{
id,
}}
/>
</MockedProvider>
);
await waitFor(() => {
const items = screen.getAllByTestId('succession-line-item');
expect(items.length).toBe(3);
expect(
items[0].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('abc');
expect(
items[1].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('def');
expect(
items[2].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('ghi');
expect(
items[number - 1].querySelector('[data-testid="icon-bullet"]')
).toBeInTheDocument();
});
}
);
});
@@ -1,11 +1,17 @@
import type { ReactNode } from 'react';
import { useState } from 'react';
import { Fragment, useState } from 'react';
import { useMemo } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { marketDataProvider } from '../../market-data-provider';
import { totalFeesPercentage } from '../../market-utils';
import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import {
ExternalLink,
Splash,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
formatNumber,
@@ -23,14 +29,26 @@ import BigNumber from 'bignumber.js';
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
import { ConditionOperatorMapping } from '@vegaprotocol/types';
import { MarketTradingModeMapping } from '@vegaprotocol/types';
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
import {
DApp,
FLAGS,
TOKEN_PROPOSAL,
useEnvironment,
useLinks,
} from '@vegaprotocol/environment';
import type { Provider } from '../../oracle-schema';
import { OracleBasicProfile } from '../../components/oracle-basic-profile';
import { useOracleProofs } from '../../hooks';
import { OracleDialog } from '../oracle-dialog/oracle-dialog';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useParentMarketIdQuery } from '../../__generated__';
import {
useParentMarketIdQuery,
useSuccessorMarketIdsQuery,
useSuccessorMarketQuery,
} from '../../__generated__';
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
import classNames from 'classnames';
import compact from 'lodash/compact';
type MarketInfoProps = {
market: MarketInfo;
@@ -191,6 +209,126 @@ export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
);
};
const SuccessionLineItem = ({
marketId,
isCurrent,
}: {
marketId: string;
isCurrent?: boolean;
}) => {
const { data } = useSuccessorMarketQuery({
variables: {
marketId,
},
});
const marketData = data?.market;
const governanceLink = useLinks(DApp.Token);
const proposalLink = marketData?.proposal?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', marketData?.proposal?.id))
: undefined;
return (
<div
data-testid="succession-line-item"
className={classNames(
'rounded p-2 bg-vega-clight-700 dark:bg-vega-cdark-700',
'font-alpha',
'flex flex-col '
)}
>
<div className="flex justify-between">
<div>
{marketData ? (
proposalLink ? (
<ExternalLink href={proposalLink}>
{marketData.tradableInstrument.instrument.code}
</ExternalLink>
) : (
marketData.tradableInstrument.instrument.code
)
) : (
<span className="block w-20 h-4 mb-1 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
)}
</div>
{isCurrent && (
<Tooltip description={t('This market')}>
<div className="text-vega-clight-200 dark:text-vega-cdark-200 cursor-help">
<VegaIcon name={VegaIconNames.BULLET} size={16} />
</div>
</Tooltip>
)}
</div>
<div className="text-xs">
{marketData ? (
marketData.tradableInstrument.instrument.name
) : (
<span className="block w-28 h-4 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
)}
</div>
<div
data-testid="succession-line-item-market-id"
className="text-xs truncate mt-1"
>
{marketId}
</div>
</div>
);
};
const SuccessionLink = () => (
<div className="text-center leading-none" aria-hidden>
<VegaIcon name={VegaIconNames.ARROW_DOWN} size={12} />
</div>
);
const buildSuccessionLine = (
all: {
id: string;
successorMarketID?: string | null | undefined;
parentMarketID?: string | null | undefined;
}[],
id: string
) => {
let line = [id];
const find = (id: string, dir?: 'up' | 'down') => {
const item = all.find((a) => a.id === id);
const anc = dir === 'up' && item?.parentMarketID;
const des = dir === 'down' && item?.successorMarketID;
if (anc) {
line = [anc, ...line];
find(anc, 'up');
}
if (des) {
line = [...line, des];
find(des, 'down');
}
};
find(id, 'up');
find(id, 'down');
return line;
};
export const SuccessionLineInfoPanel = ({
market,
}: {
market: Pick<MarketInfo, 'id'>;
}) => {
const { data } = useSuccessorMarketIdsQuery();
const ids = compact(data?.marketsConnection?.edges.map((e) => e.node));
const line = buildSuccessionLine(ids, market.id);
return (
<div className="flex flex-col gap-2">
{line.map((id, i) => (
<Fragment key={i}>
{i > 0 && <SuccessionLink />}
<SuccessionLineItem marketId={id} isCurrent={id === market.id} />
</Fragment>
))}
</div>
);
};
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
<MarketInfoTable
data={{
@@ -1,11 +1,9 @@
import React, { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { MarketMaybeWithData } from '../../markets-provider';
import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs';
@@ -43,14 +41,15 @@ const defaultColDef = {
filterParams: { buttons: ['reset'] },
minWidth: 100,
};
export const MarketListTable = forwardRef<
AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
}
>(({ onMarketClick, SuccessorMarketRenderer, ...props }, ref) => {
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
};
export const MarketListTable = ({
onMarketClick,
SuccessorMarketRenderer,
...props
}: Props) => {
const columnDefs = useColumnDefs({ onMarketClick });
const components = {
PriceFlashCell,
@@ -61,7 +60,6 @@ export const MarketListTable = forwardRef<
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={getRowId}
ref={ref}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
@@ -69,6 +67,6 @@ export const MarketListTable = forwardRef<
{...props}
/>
);
});
};
export default MarketListTable;
@@ -1,6 +1,5 @@
import type { MouseEvent } from 'react';
import React, { useEffect, useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import React, { useEffect } from 'react';
import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import { MarketListTable } from './market-list-table';
@@ -18,8 +17,6 @@ export const MarketsContainer = ({
onSelect,
SuccessorMarketRenderer,
}: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, reload } = useDataProvider({
dataProvider,
variables: undefined,
@@ -37,7 +34,6 @@ export const MarketsContainer = ({
return (
<div className="h-full relative">
<MarketListTable
ref={gridRef}
rowData={data}
onCellClicked={(cellEvent: CellClickedEvent) => {
const { data, column, event } = cellEvent;
@@ -1 +0,0 @@
export * from './order-actions-dropdown';
@@ -1,13 +0,0 @@
import {
ActionsDropdown,
TradingDropdownCopyItem,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const OrderActionsDropdown = ({ id }: { id: string }) => {
return (
<ActionsDropdown data-testid="order-actions-content">
<TradingDropdownCopyItem value={id} text={t('Copy order ID')} />
</ActionsDropdown>
);
};
@@ -14,6 +14,7 @@ import {
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
export enum Filter {
'Open' = 'Open',
@@ -42,6 +43,7 @@ export const OrderListManager = ({
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const [viewOrder, setViewOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const variables =
@@ -89,6 +91,7 @@ export const OrderListManager = ({
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
@@ -134,6 +137,14 @@ export const OrderListManager = ({
}}
/>
)}
{viewOrder && (
<OrderViewDialog
isOpen={Boolean(viewOrder)}
order={viewOrder}
onChange={() => setViewOrder(null)}
onMarketClick={onMarketClick}
/>
)}
</>
);
};
@@ -27,6 +27,7 @@ const defaultProps: OrderListTableProps = {
rowData: [],
onEdit: jest.fn(),
onCancel: jest.fn(),
onView: jest.fn(),
isReadOnly: false,
};
@@ -22,6 +22,9 @@ const Template: Story = (args) => {
onEdit={() => {
return;
}}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>
@@ -49,6 +52,9 @@ const Template2: Story = (args) => {
rowData={args.data}
onCancel={cancel}
onEdit={setEditOrder}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>
@@ -6,7 +6,14 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import {
ActionsDropdown,
ButtonLink,
TradingDropdownCopyItem,
DropdownMenuItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react';
import { memo, forwardRef, useMemo } from 'react';
import {
@@ -27,7 +34,6 @@ import type {
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import { OrderActionsDropdown } from '../order-actions-dropdown';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
@@ -35,6 +41,7 @@ export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
onCancel: (order: Order) => void;
onEdit: (order: Order) => void;
onView: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
filter?: Filter;
@@ -46,7 +53,15 @@ export const OrderListTable = memo<
>(
forwardRef<AgGridReact, OrderListTableProps>(
(
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props },
{
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
filter,
...props
},
ref
) => {
const showAllActions = props.isReadOnly
@@ -295,7 +310,20 @@ export const OrderListTable = memo<
</ButtonLink>
</>
)}
<OrderActionsDropdown id={data?.id} />
<ActionsDropdown data-testid="market-actions-content">
<TradingDropdownCopyItem
value={data.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() => onView(data)}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</ActionsDropdown>
</div>
);
},
@@ -305,6 +333,7 @@ export const OrderListTable = memo<
filter,
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
props.isReadOnly,
@@ -0,0 +1,177 @@
import { render, screen } from '@testing-library/react';
import { OrderViewDialog } from './order-view-dialog';
import type { Order } from '../order-data-provider';
import { BrowserRouter } from 'react-router-dom';
import {
MarketState,
MarketTradingMode,
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
} from '@vegaprotocol/types';
describe('OrderViewDialog', () => {
it('should render the order view dialog if the order is provided', async () => {
const order: Order = {
id: '7f2a78f370062e41683d26a0fcc4521b2bd4b747530b78bc6bc86195db0e5fb3',
market: {
__typename: 'Market',
id: 'b66cd4be223dfd900a4750bb5175e17d8f678996877d262be4c749a99e22a970',
decimalPlaces: 5,
positionDecimalPlaces: 3,
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
makerFee: '0.0002',
infrastructureFee: '0.0005',
liquidityFee: '0.001',
},
},
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'Tesla Quarterly (Sep 2023)',
code: 'TSLA.QM21',
metadata: {
__typename: 'InstrumentMetadata',
tags: [
'formerly:5A86B190C384997F',
'quote:EURO',
'ticker:TSLA',
'class:equities/single-stock-futures',
'sector:tech',
'listing_venue:NASDAQ',
'country:US',
'auto:tsla',
],
},
product: {
__typename: 'Future',
settlementAsset: {
__typename: 'Asset',
id: '177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
symbol: 'tEURO',
name: 'tEURO Fairground',
decimals: 5,
quantum: '1',
},
quoteName: 'EURO',
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '3f5941ba047dabb3bec23f112db0a8cc8aecbbbc6b34d1366d9383ed1b0f39df',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '05ca5485678aa7c705aa6a683fd062714cbcfdac2d1a27641cd27b66168d9c1d',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.TSLA.value',
tradingTerminationProperty: 'termination.TSLA.value',
},
},
},
},
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '2023-07-19T12:05:25.822854221Z',
close: null,
},
},
type: OrderType.TYPE_LIMIT,
side: Side.SIDE_BUY,
size: '10000',
status: OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
price: '15000000',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
remaining: '5000',
expiresAt: null,
createdAt: '2023-08-01T14:47:34.977742Z',
updatedAt: null,
postOnly: false,
reduceOnly: false,
liquidityProvision: null,
peggedOrder: null,
icebergOrder: {
__typename: 'IcebergOrder',
peakSize: '5000',
minimumVisibleSize: '2000',
reservedRemaining: '5000',
},
__typename: 'Order',
};
render(
<BrowserRouter>
<OrderViewDialog order={order} onChange={jest.fn()} isOpen={true} />
</BrowserRouter>
);
expect(screen.getByTestId('order-market-label')).toHaveTextContent(
'Market'
);
expect(screen.getByTestId('order-market-value')).toHaveTextContent(
'Tesla Quarterly (Sep 2023)'
);
expect(screen.getByTestId('order-type-label')).toHaveTextContent('Type');
expect(screen.getByTestId('order-type-value')).toHaveTextContent('Limit');
expect(screen.getByTestId('order-price-label')).toHaveTextContent('Price');
expect(screen.getByTestId('order-price-value')).toHaveTextContent('150.00');
expect(screen.getByTestId('order-size-label')).toHaveTextContent('Size');
expect(screen.getByTestId('order-size-value')).toHaveTextContent('+10.00');
expect(screen.getByTestId('order-remaining-label')).toHaveTextContent(
'Remaining'
);
expect(screen.getByTestId('order-remaining-value')).toHaveTextContent(
'+5.00'
);
expect(
screen.getByTestId('order-iceberg-order-reserved-remaining-value')
).toHaveTextContent('5.00');
});
});
@@ -0,0 +1,280 @@
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {
ButtonLink,
Dialog,
KeyValueTable,
KeyValueTableRow,
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import type { Order } from '../order-data-provider';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
interface OrderViewDialogProps {
isOpen: boolean;
order: Order;
onChange: (open: boolean) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
export const OrderViewDialog = ({
isOpen,
order,
onChange,
onMarketClick,
}: OrderViewDialogProps) => {
const [, setCopied] = useCopyTimeout();
return (
<Dialog open={isOpen} title={t('Order details')} onChange={onChange}>
<KeyValueTable>
<KeyValueTableRow key={'order-market'}>
<div data-testid={'order-market-label'}>{t('Market')}</div>
<div data-testid={`order-market-value`}>
{onMarketClick ? (
<ButtonLink
onClick={() => order.market && onMarketClick(order.market?.id)}
>
{order.market?.tradableInstrument.instrument.name}
</ButtonLink>
) : (
order.market?.tradableInstrument.instrument.name
)}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-type'}>
<div data-testid={'order-type-label'}>{t('Type')}</div>
<div data-testid={`order-type-value`}>
{Schema.OrderTypeMapping[order.type as Schema.OrderType]}
</div>
</KeyValueTableRow>
{order.market && (
<KeyValueTableRow key={'order-price'}>
<div data-testid={'order-price-label'}>{t('Price')}</div>
<div data-testid={`order-price-value`}>
{addDecimalsFormatNumber(
order.price,
order.market.decimalPlaces as number
)}
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-size'}>
<div data-testid={'order-size-label'}>{t('Size')}</div>
<div data-testid={`order-size-value`}>
<Size
value={order.size}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-remaining'} className="mb-4">
<div data-testid={'order-remaining-label'}>{t('Remaining')}</div>
<div data-testid={`order-remaining-value`}>
<Size
value={order.remaining}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-status'}>
<div data-testid={'order-status-label'}>{t('Status')}</div>
<div data-testid={`order-status-value`}>
{Schema.OrderStatusMapping[order.status as Schema.OrderStatus]}
</div>
</KeyValueTableRow>
{order.rejectionReason && (
<KeyValueTableRow key={'order-rejection-reason'}>
<div data-testid={'order-rejection-reason-label'}>
{t('rejection reason')}
</div>
<div data-testid={`order-rejection-reason-value`}>
{
Schema.OrderRejectionReasonMapping[
order.rejectionReason as Schema.OrderRejectionReason
]
}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-id'}>
<div data-testid={'order-id-label'}>{t('Order ID')}</div>
<div data-testid={`order-id-value`}>
{truncateMiddle(order.id, 10)}
<CopyToClipboard text={order.id} onCopy={() => setCopied(true)}>
<button
type="button"
data-testid="copy-order-id"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-created'}>
<div data-testid={'order-created-label'}>{t('Created')}</div>
<div data-testid={`order-created-value`}>
{getDateTimeFormat().format(new Date(order.createdAt))}
</div>
</KeyValueTableRow>
{order.updatedAt && (
<KeyValueTableRow key={'order-updated'}>
<div data-testid={'order-updated-label'}>{t('Updated')}</div>
<div data-testid={`order-updated-value`}>
{getDateTimeFormat().format(new Date(order.updatedAt))}
</div>
</KeyValueTableRow>
)}
{order.expiresAt && (
<KeyValueTableRow key={'order-expires'}>
<div data-testid={'order-expires-label'}>{t('Expires')}</div>
<div data-testid={`order-expires-value`}>
{getDateTimeFormat().format(new Date(order.expiresAt))}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-time-in-force'} className="mt-4">
<div data-testid={'order-time-in-force-label'}>
{t('Time in force')}
</div>
<div data-testid={`order-time-in-force-value`}>
{
Schema.OrderTimeInForceMapping[
order.timeInForce as Schema.OrderTimeInForce
]
}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-post-only'}>
<div data-testid={'order-post-only-label'}>{t('Post only')}</div>
<div data-testid={`order-post-only-value`}>
{order.postOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-reduce-only'}>
<div data-testid={'order-reduce-only-label'}>{t('Reduce only')}</div>
<div data-testid={`order-reduce-only-value`}>
{order.reduceOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-pegged'}>
<div data-testid={'order-pegged-label'}>{t('Pegged')}</div>
<div data-testid={`order-pegged-value`}>
{order.peggedOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-liquidity-provision'}>
<div data-testid={'order-liquidity-provision-label'}>
{t('Liquidity provision')}
</div>
<div data-testid={`order-liquidity-provision-value`}>
{order.liquidityProvision ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
</KeyValueTable>
<KeyValueTableRow key={'order-iceberg-order'}>
<div data-testid={'order-iceberg-order-label'}>
{t('Iceberg order')}
</div>
<div data-testid={`order-iceberg-order-value`}>
{order.icebergOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-peak-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-peak-size-label'}>
<Tooltip
description={t(
'The maximum volume that can be traded at once. Must be less than the total size of the order.'
)}
>
<span>{t('Peak size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-peak-size-value`}>
<Size
value={order.icebergOrder.peakSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-minimum-visible-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-minimum-visible-size-label'}>
<Tooltip
description={t(
'When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.'
)}
>
<span>{t('Minimum size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-minimum-visible-size-value`}>
<Size
value={order.icebergOrder.minimumVisibleSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-reserved-remaining'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-reserved-remaining-label'}>
{t('Reserved remaining')}
</div>
<div data-testid={`order-iceberg-order-reserved-remaining-value`}>
<Size
value={order.icebergOrder.reservedRemaining}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
</Dialog>
);
};
@@ -2,17 +2,13 @@ import { render, screen, act } from '@testing-library/react';
import { StopOrdersManager } from './stop-orders-manager';
import * as useDataProviderHook from '@vegaprotocol/data-provider';
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
import * as stopOrdersTableMock from '../stop-orders-table/stop-orders-table';
import { forwardRef } from 'react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
// @ts-ignore StopOrdersTable is read only but we need to override with the forwardRef to
// avoid warnings about padding refs
stopOrdersTableMock.StopOrdersTable = forwardRef(() => (
<div>StopOrdersTable</div>
));
jest.mock('../stop-orders-table/stop-orders-table', () => ({
StopOrdersTable: () => <div>StopOrdersTable</div>,
}));
const generateJsx = () => {
const pubKey = '0x123';
@@ -8,7 +8,7 @@ import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react';
import { memo, forwardRef, useMemo } from 'react';
import { memo, useMemo } from 'react';
import {
AgGridLazy as AgGrid,
SetFilter,
@@ -36,265 +36,252 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
export const StopOrdersTable = memo<
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
>(
forwardRef<AgGridReact, StopOrdersTableProps>(
({ onCancel, onMarketClick, ...props }, ref) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
minWidth: 150,
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
if (data && value?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(
value.price,
data.market.decimalPlaces
)}`;
}
if (
data &&
value?.__typename === 'StopOrderTrailingPercentOffset'
) {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return '-';
},
minWidth: 100,
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
minWidth: 150,
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
>(({ onCancel, onMarketClick, ...props }: StopOrdersTableProps) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
minWidth: 150,
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
if (data && value?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(
value.price,
data.market.decimalPlaces
)}`;
}
if (data && value?.__typename === 'StopOrderTrailingPercentOffset') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return '-';
},
minWidth: 100,
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
minWidth: 150,
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
);
},
minWidth: 80,
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
minWidth: 80,
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
),
minWidth: 100,
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
minWidth: 100,
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<
StopOrder,
'submission.timeInForce'
>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
minWidth: 150,
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
minWidth: 150,
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
)
);
},
minWidth: 80,
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
minWidth: 80,
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
),
minWidth: 100,
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
minWidth: 100,
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
minWidth: 150,
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
minWidth: 150,
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
</ButtonLink>
)}
</div>
);
},
},
],
[onCancel, onMarketClick, props.isReadOnly, showAllActions]
);
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
</ButtonLink>
)}
</div>
);
},
},
],
[onCancel, onMarketClick, props.isReadOnly, showAllActions]
);
return (
<AgGrid
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
columnDefs={columnDefs}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
}
)
);
return (
<AgGrid
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
columnDefs={columnDefs}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
});
+310 -266
View File
@@ -41,285 +41,329 @@ const singleRow: Position = {
const singleRowData = [singleRow];
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
<PositionsTable rowData={[]} isReadOnly={false} />
);
expect(baseElement).toBeTruthy();
});
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
describe('Positions', () => {
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
<PositionsTable rowData={[]} isReadOnly={false} />
);
expect(baseElement).toBeTruthy();
});
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
'Margin allocated',
'Realised PNL',
'Unrealised PNL',
'Updated',
]);
});
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
});
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
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();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(12);
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();
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
'Margin allocated',
'Realised PNL',
'Unrealised PNL',
'Updated',
]);
});
it('renders value if no status is normal', () => {
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
it('Does not fail if the market name does not match the split pattern', async () => {
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
const row = [
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
expect(screen.getByText(breakingMarketName)).toBeTruthy();
});
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('1,230.0');
});
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
<PositionsTable
rowData={[
{
...singleRow,
marketTradingMode:
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
it('displays realised and unrealised PNL', async () => {
// pnl cells should be rendered with asset dps
const expectedRealised = addDecimalsFormatNumber(
singleRow.realisedPNL,
singleRow.decimals
);
const expectedUnrealised = addDecimalsFormatNumber(
singleRow.unrealisedPNL,
singleRow.decimals
);
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual(expectedRealised);
expect(cells[10].textContent).toEqual(expectedUnrealised);
});
it('displays close button', async () => {
await act(async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
await act(async () => {
render(
<PositionsTable
rowData={[{ ...singleRow, openVolume: '0' }]}
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
});
describe('PNLCell', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
data: undefined,
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
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();
expect(
within(tooltip).getByText(
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
)
).toBeInTheDocument();
});
});
it('renders status with warning tooltip if not normal', async () => {
describe('OpenVolumeCell', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
data: undefined,
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();
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 orders were closed', 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();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
)
).toBeInTheDocument();
});
it('renders status with warning tooltip if position was closed out', async () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
},
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();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
)
).toBeInTheDocument();
});
});
});
+395 -380
View File
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { forwardRef, useMemo } from 'react';
import { useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community';
import type {
@@ -33,7 +33,6 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
@@ -87,383 +86,367 @@ AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`;
export const PositionsTable = forwardRef<AgGridReact, Props>(
(
{
onClose,
onMarketClick,
multipleKeys,
isReadOnly,
pubKeys,
pubKey,
...props
},
ref
) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No positions')}
getRowId={getRowId}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
tooltipComponent: TooltipCellComponent,
}}
components={{
AmountCell,
PriceFlashCell,
ProgressBarCell,
MarketNameCell,
}}
{...props}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
export const PositionsTable = ({
onClose,
onMarketClick,
multipleKeys,
isReadOnly,
pubKeys,
pubKey,
...props
}: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No positions')}
getRowId={getRowId}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
tooltipComponent: TooltipCellComponent,
}}
components={{
AmountCell,
PriceFlashCell,
ProgressBarCell,
MarketNameCell,
}}
{...props}
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(
data.notional,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
minWidth: 80,
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
)
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
);
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
title={t('View settlement asset details')}
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
);
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
title={t('View settlement asset details')}
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? '' : formatNumber(value.toString(), 1),
minWidth: 100,
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
multipleKeys
? null
: {
headerName: t('Margin allocated'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
multipleKeys
? null
: {
headerName: t('Margin allocated'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
minWidth: 100,
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
minWidth: 100,
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
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,
minWidth: 100,
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
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,
minWidth: 100,
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionActionsDropdown assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
);
}
);
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionActionsDropdown assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
);
};
export default PositionsTable;
@@ -495,6 +478,12 @@ export const PNLCell = ({
<p className="mb-2">
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
</p>
<p className="mb-2">
{t(
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
[data.assetSymbol]
)}
</p>
{LOSS_SOCIALIZATION_LINK && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
@@ -516,27 +505,51 @@ export const OpenVolumeCell = ({
return <>-</>;
}
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{valueFormatted}</>;
}
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
[data.assetSymbol]
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<WarningCell
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
tooltipContent={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<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
]
)}
{t('Status: %s', PositionStatusMapping[data.status])}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
@@ -554,15 +567,17 @@ export const OpenVolumeCell = ({
const WarningCell = ({
children,
tooltipContent,
showIcon = true,
}: {
children: ReactNode;
tooltipContent: ReactNode;
showIcon?: boolean;
}) => {
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} />
{showIcon && <Icon name="warning-sign" size={3} />}
</span>
<span className="text-ellipsis overflow-hidden">{children}</span>
</div>
@@ -1,9 +1,7 @@
import React from 'react';
import { useRef } from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import type { AgGridReact } from 'ag-grid-react';
import { useColumnDefs } from './use-column-defs';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
@@ -25,7 +23,6 @@ interface ProposalListProps {
export const ProposalsList = ({
SuccessorMarketRenderer,
}: ProposalListProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data } = useProposalsListQuery({
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
@@ -40,7 +37,6 @@ export const ProposalsList = ({
return (
<div className="relative h-full">
<AgGrid
ref={gridRef}
className="w-full h-full"
columnDefs={columnDefs}
rowData={filteredData}
-4
View File
@@ -1,6 +1,4 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
import { tradesWithMarketProvider } from './trades-data-provider';
import { TradesTable } from './trades-table';
import { useCreateOrderStore } from '@vegaprotocol/orders';
@@ -11,7 +9,6 @@ interface TradesContainerProps {
}
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const useOrderStoreRef = useCreateOrderStore();
const updateOrder = useOrderStoreRef((store) => store.update);
@@ -22,7 +19,6 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
return (
<TradesTable
ref={gridRef}
rowData={data}
onClick={(price?: string) => {
if (price) {
+80 -85
View File
@@ -1,7 +1,5 @@
import type { AgGridReact } from 'ag-grid-react';
import { useMemo } from 'react';
import type { ColDef } from 'ag-grid-community';
import { forwardRef } from 'react';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
@@ -48,90 +46,87 @@ interface Props extends AgGridReactProps {
onClick?: (price?: string) => void;
}
export const TradesTable = forwardRef<AgGridReact, Props>(
({ onClick, ...props }, ref) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Price'),
field: 'price',
type: 'rightAligned',
width: 130,
cellClass: changeCellClass,
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Trade, 'price'>) => {
if (!value || !data?.market) {
return '';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<Trade, 'price'>) => {
if (!data?.market || !value) {
return '';
}
return (
<button
onClick={() =>
onClick &&
onClick(addDecimal(value, data.market?.decimalPlaces || 0))
}
className="hover:dark:bg-vega-cdark-800 hover:bg-vega-clight-800"
>
{addDecimalsFormatNumber(value, data.market.decimalPlaces)}
</button>
);
},
export const TradesTable = ({ onClick, ...props }: Props) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Price'),
field: 'price',
type: 'rightAligned',
width: 130,
cellClass: changeCellClass,
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Trade, 'price'>) => {
if (!value || !data?.market) {
return '';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
{
headerName: t('Size'),
field: 'size',
width: 125,
type: 'rightAligned',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Trade, 'size'>) => {
if (!value || !data?.market) {
return '';
}
return addDecimalsFormatNumber(
value,
data.market.positionDecimalPlaces
);
},
cellRenderer: NumericCell,
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<Trade, 'price'>) => {
if (!data?.market || !value) {
return '';
}
return (
<button
onClick={() =>
onClick &&
onClick(addDecimal(value, data.market?.decimalPlaces || 0))
}
className="hover:dark:bg-vega-cdark-800 hover:bg-vega-clight-800"
>
{addDecimalsFormatNumber(value, data.market.decimalPlaces)}
</button>
);
},
{
headerName: t('Created at'),
field: 'createdAt',
type: 'rightAligned',
width: 170,
cellClass: 'text-right',
valueFormatter: ({
},
{
headerName: t('Size'),
field: 'size',
width: 125,
type: 'rightAligned',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Trade, 'size'>) => {
if (!value || !data?.market) {
return '';
}
return addDecimalsFormatNumber(
value,
}: VegaValueFormatterParams<Trade, 'createdAt'>) => {
return value && getDateTimeFormat().format(new Date(value));
},
data.market.positionDecimalPlaces
);
},
],
[onClick]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
ref={ref}
defaultColDef={{
flex: 1,
}}
columnDefs={columnDefs}
{...props}
/>
);
}
);
cellRenderer: NumericCell,
},
{
headerName: t('Created at'),
field: 'createdAt',
type: 'rightAligned',
width: 170,
cellClass: 'text-right',
valueFormatter: ({
value,
}: VegaValueFormatterParams<Trade, 'createdAt'>) => {
return value && getDateTimeFormat().format(new Date(value));
},
},
],
[onClick]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
defaultColDef={{
flex: 1,
}}
columnDefs={columnDefs}
{...props}
/>
);
};
@@ -0,0 +1,8 @@
export const IconEye = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M0.678395 7.44128C2.71173 4.52663 5.34817 2.99829 7.9894 2.99829C11.0914 2.99829 13.7635 5.01915 15.34 7.45404L15.3408 7.45519C15.4451 7.61729 15.5006 7.80599 15.5006 7.99876C15.5006 8.19118 15.4453 8.37954 15.3414 8.54144C13.7662 11.0077 11.1114 12.9983 7.9894 12.9983C4.8342 12.9983 2.23054 11.0118 0.659261 8.55277C0.552495 8.38698 0.497119 8.19332 0.500115 7.99613C0.503119 7.79845 0.564651 7.6061 0.676935 7.44338L0.67839 7.44127L0.678395 7.44128ZM1.5 8.01133L1.50139 8.01348L1.50139 8.01349C2.95267 10.2852 5.27875 11.9983 7.9894 11.9983C10.6697 11.9983 13.0459 10.278 14.4989 8.00262L14.4999 8.00113C14.5001 8.00073 14.5003 8.0003 14.5005 7.99985C14.5005 7.9995 14.5006 7.99913 14.5006 7.99876C14.5006 7.99792 14.5003 7.9971 14.4999 7.99639L14.9203 7.72579L14.5006 7.99754C13.0421 5.74493 10.6461 3.99829 7.9894 3.99829C5.75997 3.99829 3.40017 5.28866 1.5 8.01133Z" />
<path d="M8 5.99951C6.89543 5.99951 6 6.89494 6 7.99951C6 9.10408 6.89543 9.99951 8 9.99951C9.10457 9.99951 10 9.10408 10 7.99951C10 6.89494 9.10457 5.99951 8 5.99951ZM5 7.99951C5 6.34266 6.34315 4.99951 8 4.99951C9.65685 4.99951 11 6.34266 11 7.99951C11 9.65637 9.65685 10.9995 8 10.9995C6.34315 10.9995 5 9.65637 5 7.99951Z" />
</svg>
);
};
@@ -12,6 +12,7 @@ import { IconCross } from './svg-icons/icon-cross';
import { IconDeposit } from './svg-icons/icon-deposit';
import { IconEdit } from './svg-icons/icon-edit';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconEye } from './svg-icons/icon-eye';
import { IconForum } from './svg-icons/icon-forum';
import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info';
@@ -48,6 +49,7 @@ export enum VegaIconNames {
DEPOSIT = 'deposit',
EDIT = 'edit',
EXCLAIMATION_MARK = 'exclaimation-mark',
EYE = 'eye',
FORUM = 'forum',
GLOBE = 'globe',
INFO = 'info',
@@ -88,6 +90,7 @@ export const VegaIconNameMap: Record<
deposit: IconDeposit,
edit: IconEdit,
'exclaimation-mark': IconExclaimationMark,
eye: IconEye,
forum: IconForum,
globe: IconGlobe,
info: IconInfo,
@@ -21,7 +21,7 @@ export const NotificationBanner = ({
return (
<div
className={classNames(
'flex items-center p-3 border-b min-h-[56px]',
'flex items-center px-1 py-3 border-b min-h-[56px]',
'text-[12px] leading-[16px] font-normal',
{
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
@@ -8,7 +8,7 @@ import type {
import { Intent } from '../../utils/intent';
type TradingButtonProps = {
size?: 'large' | 'medium' | 'small';
size?: 'large' | 'medium' | 'small' | 'extra-small';
intent?: Intent;
children?: ReactNode;
icon?: ReactNode;
@@ -31,6 +31,7 @@ const getClassName = (
'h-10': !subLabel && (!size || size === 'medium'),
'h-8': !subLabel && size === 'small',
'px-3 text-sm': !subLabel && size === 'small',
'h-6 px-2 text-xs': !subLabel && size === 'extra-small',
'px-4 text-base': !subLabel && size !== 'small',
'flex-col items-center justify-center px-3 pt-2.5 pb-2': subLabel,
},
@@ -1,6 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import { NotificationBanner } from '../notification-banner';
import { Intent } from '../../utils/intent';
import { TradingButton } from '../trading-button';
export function truncateMiddle(address: string, start = 6, end = 4) {
if (address.length < 11) return address;
@@ -24,19 +25,20 @@ export const ViewingAsBanner = ({
<NotificationBanner
data-testid="view-banner"
intent={Intent.None}
className="py-2 min-h-fit"
className="!px-1 !py-1 min-h-fit"
>
<div className="flex justify-between items-baseline">
<span>
{t('Viewing as Vega user:')} {pubKey && truncateMiddle(pubKey)}{' '}
</span>
<button
className="p-2 bg-light-dark-150 dark:bg-vega-dark-150 rounded uppercase"
<TradingButton
intent={Intent.None}
size="extra-small"
data-testid="exit-view"
onClick={disconnect}
>
{t('Exit view as')}
</button>
</TradingButton>
</div>
</NotificationBanner>
);
+1
View File
@@ -1 +1,2 @@
export * from './connect-dialog';
export * from './view-as-dialog';
@@ -0,0 +1,40 @@
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { ViewConnectorForm } from './view-connector-form';
import type { ViewConnector } from '../connectors';
import { create } from 'zustand';
type ViewAsDialogStore = {
open: boolean;
setOpen: (open: boolean) => void;
};
export const useViewAsDialog = create<ViewAsDialogStore>()((set) => ({
open: false,
setOpen: (open) => set({ open }),
}));
type ViewAsDialogProps = {
connector: ViewConnector;
};
export const ViewAsDialog = ({ connector }: ViewAsDialogProps) => {
const open = useViewAsDialog((state) => state.open);
const setOpen = useViewAsDialog((state) => state.setOpen);
return (
<Dialog
open={open}
size="small"
onChange={(open) => {
setOpen(open);
}}
>
<ViewConnectorForm
connector={connector}
onConnect={() => {
setOpen(false);
}}
/>
</Dialog>
);
};
@@ -2,9 +2,10 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
FormGroup,
Icon,
Input,
InputError,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import type { ViewConnector } from '../connectors';
@@ -17,7 +18,7 @@ interface FormFields {
interface RestConnectorFormProps {
connector: ViewConnector;
onConnect: (connector: ViewConnector) => void;
reset: () => void;
reset?: () => void;
}
export function ViewConnectorForm({
@@ -50,13 +51,19 @@ export function ViewConnectorForm({
return (
<>
<button
onClick={reset}
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
data-testid="back-button"
>
<Icon name={'chevron-left'} ariaLabel="back" size={4} />
</button>
{reset && (
<button
onClick={reset}
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
data-testid="back-button"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
aria-label="back"
size={16}
/>
</button>
)}
<form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form">
<h1 className="text-2xl uppercase mb-6 text-center font-alpha calt">
{t('VIEW AS VEGA USER')}
@@ -1,5 +1,4 @@
import { useEffect, useRef, useState, useMemo } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
import {
addDecimalsFormatNumber,
@@ -40,7 +39,6 @@ export const WithdrawalsTable = ({
ready?: TimestampedWithdrawals;
delayed?: TimestampedWithdrawals;
}) => {
const gridRef = useRef<AgGridReact | null>(null);
const createWithdrawApproval = useEthWithdrawApprovalsStore(
(store) => store.create
);
@@ -146,7 +144,6 @@ export const WithdrawalsTable = ({
CompleteCell,
}}
suppressCellFocus
ref={gridRef}
{...props}
/>
);
+1 -1
View File
@@ -70,7 +70,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.10.0",
"pennant": "1.11.1",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+138
View File
@@ -0,0 +1,138 @@
# Wallet
A Vega wallet is required to prepare and submit transaction on Vega (place, cancel, orders etc). See the [wallet docs](https://docs.vega.xyz/docs/mainnet/concepts/vega-wallet) for more on how "crypto" wallets work.
A wallet can contain many public/private key pairs. The public part of each key pair is known the [Party](../protocol/0017-PART-party.md) sometimes just referred to as a key or public key.
The primary job(s) of a wallet is to [sign/encrypt transaction](../protocol/0022-AUTH-auth.md) (so the network can be sure they were sent by a given party) and to broadcast these transactions to a node on the network.
## Set up wallet / Restore wallet
When opening the wallet for the first time, I...
- if the wallet sends telemetry/analytics: **must** be prompted to opt into (or stay out of) analytics (<a name="0001-WALL-003" href="#0001-WALL-003">0001-WALL-003</a>)
- I can restore a wallet from a seed phrase (<a name="0001-WALL-004" href="#0001-WALL-004">0001-WALL-004</a>)
- I can create a new wallet (<a name="0001-WALL-005" href="#0001-WALL-005">0001-WALL-005</a>)
- I can view the back up phrase (<a name="0001-WALL-006" href="#0001-WALL-006">0001-WALL-006</a>)
- I can see the first key without having to "add key". (i.e. The wallet auto generates the first key from the seed phrase) (<a name="0001-WALL-008" href="#0001-WALL-008">0001-WALL-008</a>)
...so I can sign transactions
## Configure network
When using the wallet on a network, I...
- I can have Mainnet and Fairground (testnet) pre-configured (with Mainnet being the default network) (<a name="0001-WALL-009" href="#0001-WALL-009">0001-WALL-009</a>)
- I can create a new network configuration (<a name="0001-WALL-010" href="#0001-WALL-010">0001-WALL-010</a>)
- I can refine the configuration for existing networks (including the ones that come pre-configured) (<a name="0001-WALL-011" href="#0001-WALL-011">0001-WALL-011</a>)
- I can remove networks (<a name="0001-WALL-013" href="#0001-WALL-013">0001-WALL-013</a>)
...so I can broadcast transactions to, and read information from a vega network in my wallet
## Update wallet
When using an older version of a Vega wallet than the current official release, I...
- I am warned if the version I am using is not compatible with the version of Vega on the selected network, and I am given a link to get latest compatible version on github (<a name="0001-WALL-015" href="#0001-WALL-015">0001-WALL-015</a>)
... so the version of the wallet app I am using works with the network I am using
## Log in to a wallet
When using a given wallet, I...
- I can select a wallet and enter the passphrase only once per "session" (<a name="0001-WALL-016" href="#0001-WALL-016">0001-WALL-016</a>)
... so that other users of my machine can not use my wallet, and I am not asked to re-enter frequently
## Connecting to Dapps
When a dapp requests use of a wallet, I...
- I am prompted to either select a wallet or dismiss the prompt (<a name="0001-WALL-017" href="#0001-WALL-017">0001-WALL-017</a>)
- I can select whole wallet (so that new keys are automatically shared) (<a name="0001-WALL-019" href="#0001-WALL-019">0001-WALL-019</a>)
- I can enter wallet passphrase before wallet details are shared (assuming a password has not recently been entered)(<a name="0001-WALL-022" href="#0001-WALL-022">0001-WALL-022</a>)
- I can retrospectively revoke Dapp's access to a Wallet (<a name="0001-WALL-023" href="#0001-WALL-023">0001-WALL-023</a>)
... so that I can control what public keys are shared with a dapp and what dapps can prompt me to sign transactions
## Approving transactions
When a dapp sends a transaction to the wallet for signing and broadcast, I...
- I am prompted to confirm, reject or ignore the transaction (if auto-confirm is not on) (<a name="0001-WALL-024" href="#0001-WALL-024">0001-WALL-024</a>)
- I can see the details of the transaction. See [details of transaction](#transaction-detail). (<a name="0001-WALL-025" href="#0001-WALL-025">0001-WALL-025</a>)
... so I can verify that the transaction being sent is the one I want
## Transaction log
When thinking about a recent or specific transaction, I ...
- I can find a single list of all transactions, completed and ongoing, from all keys and wallets, from my current desktop session and network (<a name="0001-WALL-034" href="#0001-WALL-034">0001-WALL-034</a>)
- I can see transactions that were confirmed by the wallet user (me) (<a name="0001-WALL-035" href="#0001-WALL-035">0001-WALL-035</a>)
- I can see transactions that were rejected by the wallet user (me) (<a name="0001-WALL-036" href="#0001-WALL-036">0001-WALL-036</a>)
- If I switch network, transactions list changes to show the transactions for that network (<a name="0001-WALL-037" href="#0001-WALL-037">0001-WALL-037</a>)
- I can click a transaction in the list to see the transaction details (<a name="0001-WALL-038" href="#0001-WALL-038">0001-WALL-038</a>)
- I can see empty state when there are no transactions for this session (<a name="0001-WALL-039" href="#0001-WALL-039">0001-WALL-039</a>)
... so that I can ensure my wallet is being used appropriately and find transaction I made
## Transaction details
when looking at a specific transaction...
- I can see details of specific transactions I opened (<a name="0001-WALL-041" href="#0001-WALL-041">0001-WALL-041</a>)
- I can find my way to the transaction on block explorer (<a name="0001-WALL-042" href="#0001-WALL-042">0001-WALL-042</a>)
- I can find my way to the complete transaction history for that key on block explorer (<a name="0001-WALL-043" href="#0001-WALL-043">0001-WALL-043</a>)
- I can see [status of broadcasted transactions](0003-WTXN-submit_vega_transaction.md#track-transaction-on-network)
.. so I can find all the information about what has happened with mined and un-mined transactions
## Key management
When using a Vega wallet, I...
- I can create new keys (derived from the source of wallet) (<a name="0001-WALL-052" href="#0001-WALL-052">0001-WALL-052</a>)
- I can see full public key or be able to copy it to clipboard (<a name="0001-WALL-054" href="#0001-WALL-054">0001-WALL-054</a>)
- I can change key name/alias (<a name="0001-WALL-055" href="#0001-WALL-055">0001-WALL-055</a>)
... so I can manage risk (e.g. isolate margin), mitigate the damage of a key being compromised, or use multiple trading strategies
## Taint keys
When protecting myself from use of keys that may be compromised, I..
- I can select a key I wish to taint (<a name="0001-WALL-057" href="#0001-WALL-057">0001-WALL-057</a>)
- I am prompted to enter wallet password to taint key (<a name="0001-WALL-058" href="#0001-WALL-058">0001-WALL-058</a>)
- I can see tainted keys flagged as tainted (<a name="0001-WALL-060" href="#0001-WALL-060">0001-WALL-060</a>)
... so that tainted keys must not be used
When I have accidentally tainted a key I...
- I can select a key to un-taint and be required to enter wallet password (<a name="0001-WALL-061" href="#0001-WALL-061">0001-WALL-061</a>)
...so that I must use the key again
## Manually sign a message
When wishing to use my wallet to sign arbitrary messages, I...
- I can enter content to be signed with key (<a name="0001-WALL-062" href="#0001-WALL-062">0001-WALL-062</a>)
- I can submit/sign the content (<a name="0001-WALL-065" href="#0001-WALL-065">0001-WALL-065</a>)
- I can [track progress](0003-WTXN-submit_vega_transaction.md#track-transaction-on-network) of broadcast transaction either by being given a hash that I can use in block explorer, or see the transaction status
.. so I can control of the message being signed, and can use the message elsewhere (for example to prove I own a wallet)
## Wallet management
When seeking to reduce risk of compromise I...
- I can create multiple wallets (<a name="0001-WALL-066" href="#0001-WALL-066">0001-WALL-066</a>)
- I can switch between wallets (<a name="0001-WALL-067" href="#0001-WALL-067">0001-WALL-067</a>)
- I can remove a wallet (<a name="0001-WALL-068" href="#0001-WALL-068">0001-WALL-068</a>)
- I can change wallet name (<a name="0001-WALL-069" href="#0001-WALL-069">0001-WALL-069</a>)
... so that I must administrate my wallets
+64
View File
@@ -0,0 +1,64 @@
# Connect Vega wallet & select keys
## Connect wallet
When looking to use Vega via a user interface e.g. Dapp (Decentralized web App), I...
- If the app loads and already has a connection it can restore "eagerly" (without the user having to click connect) it **could** do so
- **must** select a connection method / wallet type: (<a name="0002-WCON-002" href="#0002-WCON-002">0002-WCON-002</a>)
- if Rest:
- **must** have the option to input a non-default Wallet location (<a name="0002-WCON-003" href="#0002-WCON-003">0002-WCON-003</a>)
- **must** submit attempt to connect to wallet (<a name="0002-WCON-005" href="#0002-WCON-005">0002-WCON-005</a>)
- if the dapp DOES already have a permission with the wallet: **must** see that wallet is connected (<a name="0002-WCON-007" href="#0002-WCON-007">0002-WCON-007</a>) note: if the user want to connect to a different wallet to the one that they were previously connected with, they will have to hit logout.
- if the app uses one key at a time: **should** show what key is active (re-use the last active key) (<a name="0002-WCON-008" href="#0002-WCON-008">0002-WCON-008</a>)
- if the wallet does NOT have an existing permission with the wallet: **must** prompt user to check wallet app to approve the request to connect wallet: See [Connecting to Dapps](0002-WCON-connect_vega_wallet.md#connect-wallet) for what should happen in wallet app (<a name="0002-WCON-009" href="#0002-WCON-009">0002-WCON-009</a>)
- if new keys are given permission: **must** show the user the keys have been approved (<a name="0002-WCON-010" href="#0002-WCON-010">0002-WCON-010</a>)
- if the dapp uses one key at a time: **should** prompt me to select key. See [select/switch keys](#select-and-switch-keys). (<a name="0002-WCON-014" href="#0002-WCON-014">0002-WCON-014</a>)
- if user rejects connection: **must** see a message saying that the request to connect was denied (<a name="0002-WCON-015" href="#0002-WCON-015">0002-WCON-015</a>)
- if the dapp is unable to connect for technical reason (e.g. CORS): **must** see an explanation of the error, and a method of fixing the issue (<a name="0002-WCON-016" href="#0002-WCON-016">0002-WCON-016</a>)
- ~~Browser wallet~~ `not available yet`
- Fairground hosted wallet
- **must** only be be shown this option if the dapp is connected to fairground (<a name="0002-WCON-039" href="#0002-WCON-039">0002-WCON-039</a>)
- **must** input a wallet name (<a name="0002-WCON-017" href="#0002-WCON-017">0002-WCON-017</a>)
- **must** input a password (<a name="0002-WCON-018" href="#0002-WCON-018">0002-WCON-018</a>)
- if success: **must** see that the wallet is connected and details of connected key (<a name="0002-WCON-019" href="#0002-WCON-019">0002-WCON-019</a>)
- if failure: **must** see reason for failure (<a name="0002-WCON-020" href="#0002-WCON-020">0002-WCON-020</a>)
- _note: the fairground hosted wallet is configured to automatically approve connections from dapps so there is no need for key selection._
- **must** have the option to select a different method / wallet type if I change my mind (<a name="0002-WCON-021" href="#0002-WCON-021">0002-WCON-021</a>)
... so I can use the interface to read data about my key/party or request my wallet to broadcast transactions to a Vega network.
## Disconnect wallet
When wishing to disconnect my wallet, I...
- **must** see an option to disconnect wallet (<a name="0002-WCON-022" href="#0002-WCON-022">0002-WCON-022</a>)
- **must** see confirmation that wallet has been disconnected (<a name="0002-WCON-023" href="#0002-WCON-023">0002-WCON-023</a>)
... so that I can protect my wallet from malicious use or select a different wallet to connect to
## Select and switch keys
when looking to do something with a specific key (or set of keys) from my wallet, I...
- **must** see what key is currently selected (if any) (<a name="0002-WCON-025" href="#0002-WCON-025">0002-WCON-025</a>)
- **must** see a list of keys that are approved from the connected wallet (<a name="0002-WCON-026" href="#0002-WCON-026">0002-WCON-026</a>)
- for each key:
- **must** see the first and last 6 digits of the [public key](DATA-data_display.md#public-keys). (<a name="0002-WCON-027" href="#0002-WCON-027">0002-WCON-027</a>)
- **must** be able to copy to clipboard the whole public key (<a name="0002-WCON-029" href="#0002-WCON-029">0002-WCON-029</a>)
- **must** see the key name/alias (meta data) (<a name="0002-WCON-030" href="#0002-WCON-030">0002-WCON-030</a>)
- **must** see the option to trigger a re-authenticate so I can use newly created keys (<a name="0002-WCON-035" href="#0002-WCON-035">0002-WCON-035</a>)
...so that I can select the key(s) that I want to use.
@@ -0,0 +1,49 @@
# Submit Vega transaction
A dapp sends a transaction to a wallet, that wallet then broadcasts the transaction to a network. Therefore the following is broken up into two steps. The transaction could fail at either. Generally: Once the transaction has gone to the network a user can use block explorer to track the transaction, but some tracking in Dapp or wallet will help.
When submitting a Vega transaction of any kind, I...
## Track transaction to wallet
if not connected to a Vega wallet:
- **must** be told that I am not connected, so can not submit a transaction, and given the [option to connect](0012-WCON-connect_vega_wallet.md). (Note: this may have happened if the wallet has become disconnected without the user knowing) (<a name="0003-WTXN-001" href="#0003-WTXN-001">0003-WTXN-001</a>)
if transaction not auto approved by wallet:
- **must** see a prompt to check connected vega wallet to approve transaction (<a name="0003-WTXN-002" href="#0003-WTXN-002">0003-WTXN-002</a>)
- **could** see the transaction details that has been passed to the wallet for broadcast
if transaction is approved by wallet:
- **must** see A [transaction hash](DATA-data_display.md#transaction-hash) (<a name="0003-WTXN-003" href="#0003-WTXN-003">0003-WTXN-003</a>)
- **must** see the public key that this transaction was submitted for (<a name="0003-WTXN-004" href="#0003-WTXN-004">0003-WTXN-004</a>)
- **should** see the alias for the key that submitted this transaction
- **could** see a prompt to set this app to [auto approve](0001-WALL-wallet.md#approving-transactions) in wallet app
if transaction is rejected by wallet:
- **could** see that the order was rejected by the connected wallet (closing the window automatically may be appropriate) (<a name="0003-WTXN-007" href="#0003-WTXN-007">0003-WTXN-007</a>)
if the wallet does not respond:
- **must** not be able prevented from using the app, e.g. you can (<a name="0003-WTXN-008" href="#0003-WTXN-008">0003-WTXN-008</a>)
- **would** like to be able to cancel the transaction from the dapp so that the wallet is no longer in the state where it is asking user to confirm
if the wallet highlights an issue with the transaction:
- **must** show that the transaction was marked as invalid by the wallet and not broadcast (aka an error was returned from Wallet) (<a name="0003-WTXN-009" href="#0003-WTXN-009">0003-WTXN-009</a>)
- **should** see the error returned highlighted in context of the form that submitted the transaction in Dapp
- **must** show error returned by wallet (<a name="0003-WTXN-011" href="#0003-WTXN-011">0003-WTXN-011</a>)
## Track transaction on network
- **must** see a link to that transaction in a block explorer for the appropriate network (<a name="0003-WTXN-012" href="#0003-WTXN-012">0003-WTXN-012</a>)
- **should** see an indication transaction status
- **should** see the network the transaction was broadcast to
- **should** see the block the transaction was processed in
- **should** show the node the transaction was broadcast to
- **could** see the validator that processed the block the transaction was processed in
... so I am aware of the transactions status of the transactions my wallet is sending and that are being processed by the network
@@ -0,0 +1,34 @@
# Connect Ethereum wallet
Dapps can connect to an Ethereum wallet to complete Ethereum transactions such as Deposits and withdraws to/from Vega, Association and more.
## Connecting wallet
When wanting or needing to write to Ethereum, I...
- will have seen a link to connect that opens connection options (this always happens in context so should be covered by WITH, DEPO, ASSO ACS)
- if first time:
- **must** select a connection method / wallet type: (e.g. wallet connect, injected / metamask) (<a name="0004-EWAL-001" href="#0004-EWAL-001">0004-EWAL-001</a>)
- **must** be prompt to check eth wallet (while the dapp waits for a response) (<a name="0004-EWAL-002" href="#0004-EWAL-002">0004-EWAL-002</a>)
- **must** see an option to cancel the attempted connection (if the wallet fails to respond) (<a name="0004-EWAL-003" href="#0004-EWAL-003">0004-EWAL-003</a>)
- if the app gets multiple keys: the user:
- **should** be shown the keys returned and given a UI to select a key for use (but the pattern is often just to select the first in the array)
- **should** be prompted to select one (in many cases Dapps default to key 0 in the array)
- after first use (if there is a connection to restore):
- **must** prompt wallet to grant access (<a name="0004-EWAL-004" href="#0004-EWAL-004">0004-EWAL-004</a>)
- **should** see previous connection has been recovered
- **should** see a link to trigger a fresh connection / fetch new keys (in in the case where I now want to use a different wallet to the one I was connected with)
- once connected:
- **must** see the connected ethereum wallet Public key (<a name="0004-EWAL-005" href="#0004-EWAL-005">0004-EWAL-005</a>)
... so I can sign and broadcast Ethereum transactions, use a key address as in input, or read data from ethereum via my connected wallet
## Disconnecting
When I'm finished using a connected Ethereum wallet I may wish to disconnect...
- **must** see a link to disconnect (<a name="0004-EWAL-006" href="#0004-EWAL-006">0004-EWAL-006</a>)
- **must** destroy dapp -> ETH wallet session so that hitting connect again triggers the modal that asks what method you'd like to use to connect to an ETH wallet, (note: it is not possible to invalidate the permission the metamask wallet has granted the app, therefore users will need to know that if they want to connect to a new ETH key they will have to do so from the wallet) (<a name="0004-EWAL-007" href="#0004-EWAL-007">0004-EWAL-007</a>)
... so that I can use a different wallet, or ensure may wallet can not be used by other apps
@@ -0,0 +1,60 @@
# Submit Ethereum transaction
## Know what transaction I'm signing
When about to click to prompt Ethereum wallet to sign a transaction, I...
- **should** see the contract address I am about to interact with
- **should** see the function name I am about to interact with
...so I know what to expect when my wallet asks me to sign
## Track transactions to wallet
after clicking to submit an eth transaction to a connected wallet, I...
- **could** see an estimate for gas prices compared to a recent history
- **could** see an estimated gas price for the function in question
- **must** see prompt to check Ethereum wallet to approve transactions
... so I know I need to go to my wallet app to approve the transaction
## ERC20 approval/permit
> The approval/permit step is part of the [ERC20 standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/). It's intention is to provide additional security when interacting with smart contracts / using Dapps. It sets a maximum amount that the given eth key can send to a given address. It means that before end "spend" of ERC20 tokens I will have need to submit a "approve" transaction to tell the network how much is approved. An attempt to spend more than the approved amount will fail.
> For example: In my approve transaction, I approve 10, I then spend 5. In my next transaction I attempt to spend 6, this will fail as my approve amount was reduced by the first transaction.
> It is common for dapps to have a approve button that simply asks to approve a massive amount and leave it up to the wallet UI (e.g. metamask) to ask the user if they would like to change this.
> Some ERC20 contracts have an additional function that runs both the approve and deposit function in one, but this is not standard.
If the transaction in question requires an ERC20 approval, I...
- if the current approved amount is less than the amount being "spent": **must** see be prompt to approve
- **could** see the current approved amount
- **must** be able to set the amount to be approved (in case the connected wallet does not handle this) (<a name="0005-ETXN-006" href="#0005-ETXN-006">0005-ETXN-006</a>)
- **must** send an approve transaction with either a user specified amount or a very large number (<a name="0005-ETXN-001" href="#0005-ETXN-001">0005-ETXN-001</a>)
- **must** see feedback of the state of approve transaction see "tracking ethereum transactions" below. (<a name="0005-ETXN-002" href="#0005-ETXN-002">0005-ETXN-002</a>)
... so I can control the maximum permitted transfer to the contract in question
## Tracking Ethereum transactions on network
After approving a transaction in my wallet app, I...
- **should** see link to the transaction on etherscan
- **must** see the transactions status (Pending, confirmed, etc) on Ethereum by reading Ethereum (via connected wallet or the back up node specified in the app) (<a name="0005-ETXN-003" href="#0005-ETXN-003">0005-ETXN-003</a>)
- if failed: **must** see why the transaction failed (e.g. didn't pay enough gas) (<a name="0005-ETXN-004" href="#0005-ETXN-004">0005-ETXN-004</a>)
- if success: **should** see how many blocks ago the transaction was confirmed by the eth node being read
... so I can see the status of the transaction and debug as appropriate
## Tracking Ethereum transactions having their affect on Vega
Note: it is common for inter-blockchain applications to wait a certain amount of blocks before crediting money, as this reduces the risk of double spend in the case of forks or chain roll backs. There is a Vega environment variable the defines how long Vega waits.
If the ethereum transaction I've just submitted changes the state of the Vega network (e.g. a deposit from eth appearing as credited to my vega key on vega), I...
- **should** see how many Ethereum blocks Vega needs to wait before changing the state of Vega
- **should** see how many blocks have passed or remain until the required number has been met
- **must** see whether the expect action has taken place on Vega (e.g. credited Vega key) (<a name="0005-ETXN-005" href="#0005-ETXN-005">0005-ETXN-005</a>)
... so I know vega has been updated as appropriate
+36
View File
@@ -0,0 +1,36 @@
# Select network and nodes
## Startup
- **Must** automatically select a node from the environments network config stored in the [networks repo](https://github.com/vegaprotocol/networks) (<a name="0006-NETW-001" href="#0006-NETW-001">0006-NETW-001</a>)
## Network switcher
- **Must** see current network (<a name="0006-NETW-002" href="#0006-NETW-002">0006-NETW-002</a>)
- **Must** be able to change network (<a name="0006-NETW-003" href="#0006-NETW-003">0006-NETW-003</a>)
## Node health
- **Must** see node status
- Operational if node is less than 3 blocks behind (<a name="0006-NETW-004" href="#0006-NETW-004">0006-NETW-004</a>)
- Warning if greater than 3 blocks behind (<a name="0006-NETW-005" href="#0006-NETW-005">0006-NETW-005</a>)
- Warning if vega time is 3 seconds behind current time (<a name="0006-NETW-006" href="#0006-NETW-006">0006-NETW-006</a>)
- Prominent error if vega time is 10 seconds behind current time (<a name="0006-NETW-007" href="#0006-NETW-007">0006-NETW-007</a>)
- **Must** see current connected node (<a name="0006-NETW-008" href="#0006-NETW-008">0006-NETW-008</a>)
- **Must** see current block height (<a name="0006-NETW-009" href="#0006-NETW-009">0006-NETW-009</a>)
- **Must** see block height progressing (<a name="0006-NETW-010" href="#0006-NETW-010">0006-NETW-010</a>)
- **Must** see link to status and incidents site (<a name="0006-NETW-011" href="#0006-NETW-011">0006-NETW-011</a>)
## Node switcher
- **Must** be able to click on current node to open node switcher dialog (<a name="0006-NETW-012" href="#0006-NETW-012">0006-NETW-012</a>)
- In the node dialog
- **Must** must see all nodes provided by the [network config](https://github.com/vegaprotocol/networks) (<a name="0006-NETW-013" href="#0006-NETW-013">0006-NETW-013</a>)
- For each node
- **Must** see the response time of the node (<a name="0006-NETW-014" href="#0006-NETW-014">0006-NETW-014</a>)
- **Must** see the current block height (<a name="0006-NETW-015" href="#0006-NETW-015">0006-NETW-015</a>)
- **Must** see if subscriptions are working for that node (<a name="0006-NETW-016" href="#0006-NETW-016">0006-NETW-016</a>)
- **Must** be able to select and connect to any node, regardless of response time, block height or subscription status (<a name="0006-NETW-017" href="#0006-NETW-017">0006-NETW-017</a>)
- **Must** be able to select 'other' to input a node address and connect to it (<a name="0006-NETW-018" href="#0006-NETW-018">0006-NETW-018</a>)
- **Must** have disabled connect button if 'other' is selected but no url has been entered (<a name="0006-NETW-019" href="#0006-NETW-019">0006-NETW-019</a>)
- **Must** have disabled connect button if selected node is the current node (<a name="0006-NETW-020" href="#0006-NETW-020">0006-NETW-020</a>)
+40
View File
@@ -0,0 +1,40 @@
# Deposit
The Vega network has no native assets. All settlement assets exist on another chain and are "bridged" to Vega in one way or another.
In the case of [ERC20 tokens](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) there is a smart contract on the Ethereum network that acts as a vault (aka bridge) for the tokens that are deposited to Vega. The Vega network then reads the information from this vault about what Vega key to credit these tokens to. While in the Vault the Vega key that owns them (and consequently the ethereum key) may change. The vault then manages how much each ethereum key is able to withdraw from the vault given then changes in ownership that may have happened on Vega. The keys to this vault and managed by the different nodes that make up the Vega network. They verify the the appropriate amounts can be withdrawn by each Ethereum key. At time of writing only ERC20 tokens have been implemented but the pattern is likely the same for other assets/networks.
## ERC20 deposits
Note: ERC20 assets require an approval transaction to be finalised before funds can be credited to another key. Read more about approvals [link 1](https://medium.com/ethex-market/erc20-approve-allow-explained-88d6de921ce9), [link 2](https://hackernoon.com/erc20-infinite-approval-a-battle-between-convenience-and-security-lk60350r).
When making to deposit ERC20 assets to an Vega key, I...
- **should** be able to follow a link from an asset (e.g. on a market page) to the deposit form pre-populated with the given asset
- **must** see a link to [connect an ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) that I want to deposit from (<a name="1001-DEPO-001" href="#1001-DEPO-001">1001-DEPO-001</a>)
- **must** select the [asset](9001-DATA-data_display.md#asset) that I want to deposit (<a name="1001-DEPO-002" href="#1001-DEPO-002">1001-DEPO-002</a>)
- **should** easily see the assets that I have a non-zero balance for (in the connected eth wallet)
- **should** see the ERC20 token address of the asset
- **should** see the [Vega asset symbol](9001-DATA-data_display.md#asset-symbol)
- **should** see the [Vega asset name](9001-DATA-data_display.md#asset-name)
- **must** select the [amount of the asset](9001-DATA-data_display.md#asset-balances) that I want to deposit (<a name="1001-DEPO-003" href="#1001-DEPO-003">1001-DEPO-003</a>)
- **should** see an ability to populate the input with the full balance in the connected wallet
- **must** be warned if the amount being deposited is greater than the balance of the token in the connected Eth wallet (<a name="1001-DEPO-004" href="#1001-DEPO-004">1001-DEPO-004</a>)
- **must** select the [Vega key](9001-DATA-data_display.md#public-keys) that I wish to deposit to (<a name="1001-DEPO-005" href="#1001-DEPO-005">1001-DEPO-005</a>)
- **should** be able to [connect to a Vega wallet and select a key](0002-WCON-connect_vega_wallet.md#select-and-switch-keys)
- **should** be easily (if not automatically) pre-populated with a [currently connected and active Vega key](0002-WCON-connect_vega_wallet.md#select-and-switch-keys)
- **should** be able to input a Vega key other than one I am connected with (even without being connected)
- if approved amount is less than deposit:
- **must** see that an approval is needed and be prompted to approve more (<a name="1001-DEPO-006" href="#1001-DEPO-006">1001-DEPO-006</a>)
- **should** see the approved [asset amount](9001-DATA-data_display.md#asset-balances)
- **should** be able to input an amount to approve
- **must** [submit eth transaction to approve](0005-ETXN-submit_ethereum_transaction.md) (<a name="1001-DEPO-007" href="#1001-DEPO-007">1001-DEPO-007</a>)
- **must** see feedback for the approve transaction
- if approved amount is more than deposit amount:
- **could** see the approved [asset amount](9001-DATA-data_display.md#asset-balances)
- **could** set a submit a new [eth transaction to approve more or less](0005-ETXN-submit_ethereum_transaction.md)
- **must** submit the Deposit [eth transaction](0005-ETXN-submit_ethereum_transaction.md) (<a name="1001-DEPO-008" href="#1001-DEPO-008">1001-DEPO-008</a>)
- **must** see feedback on the deposit [ETH transaction](0003-WTXN-submit_vega_transaction.md) (<a name="1001-DEPO-009" href="#1001-DEPO-009">1001-DEPO-009</a>)
- **must** see feedback that the deposit has or has not been credited to the Vega key (<a name="1001-DEPO-010" href="#1001-DEPO-010">1001-DEPO-010</a>)
...so that my Vega key can use these assets on Vega
+117
View File
@@ -0,0 +1,117 @@
# Withdraw
Withdrawing funds is a two step process.
First the Vega network needs to approve that the funds can be released (not required for margin on open positions or in liquidity bond etc). If they are not, a withdraw is prepared and set aside so that it can not be used for positions etc. This also define what ethereum address will be credited the funds in step 2.
Second the user will need to run an ethereum function on the bridge contract to release the funds (and pay the gas to do so). They do this using a signature supplied by nodes of the Vega network in Step 1.
Although this is a two step process technically effort should be put into making it feel like one, then handle exceptions (like delays on withdrawals) as required.
See [Specs for eth bridge](../protocol/0031-ETHB-ethereum_bridge_spec.md) and [docs](https://docs.vega.xyz/docs/mainnet/concepts/vega-protocol#withdrawals) on withdrawals. See also the [specs on delays to withdrawals](../non-protocol-specs/0003-NP-LIMI-limits_aka_training_wheels.md#withdrawal-limits).
## Prepare an ERC20 withdraw from Vega
When wishing to withdraw some of an ERC20 asset from Vega, I...
- **should** be prompted to complete any existing incomplete withdrawals that exist for connected keys (see [complete withdrawal](#complete-erc20-withdraw-from-ethereum-bridge))
Note: It is better to encourage the completion of started withdraws as soon as possible after preparing them. This is because the validator set could theoretically change enough to make the node signatures that authorize the withdrawal invalid.
- **should** be warned that they will need to pay gas on the withdrawal before starting
- **could** show the current gas fees BEFORE preparing the withdrawal (note: shows gas estimate is a general should for all [ethereum transactions](0005-ETXN-submit_ethereum_transaction.md) but this is so a user gets to see the gas costs at step 1 assuming they will do step 2 immediately. )
Note: A user may want to delay preparing a withdrawal if gas fees on the network are particularly high at the time
- **must** select the asset to withdraw (<a name="1002-WITH-001" href="#1002-WITH-001">1002-WITH-001</a>)
- **should not** see option to select assets where I a zero [total balance](9001-DATA-data_display.md#asset-balances) (note this should also avoid `Pending` assets from appearing in the list)
- **must** see the general balance I have for that asset (<a name="1002-WITH-002" href="#1002-WITH-002">1002-WITH-002</a>)
- **should** see balances to the full number of decimal places possible for that asset
- **should** see the total balances of the assets I have
- **could** see a breakdown of other accounts I have in this asset and their balances
- **must** select the [amount](9001-DATA-data_display.md#asset-balances) of the asset I wish to withdraw (<a name="1002-WITH-003" href="#1002-WITH-003">1002-WITH-003</a>)
- **should** have an easy option (link/button) to input the full amount in general balance
- **must** be able to specify as many decimal places as the asset supports (<a name="1002-WITH-004" href="#1002-WITH-004">1002-WITH-004</a>)
- **must** be warned if the amount is greater than general balance (including if the general balance amount changes while the user is looking at the form) (<a name="1002-WITH-005" href="#1002-WITH-005">1002-WITH-005</a>)
- **must** be warned if the amount is lesser than the minimum allowed, where the minimum amount is the selected asset's quantum multiplied by the value of `spam.protection.minimumWithdrawalQuantumMultiple` network parameter (<a name="1002-WITH-026" href="#1002-WITH-026">1002-WITH-026</a>)
- **should** see a link to a faucet on the selected asset (only if there is one)
- **must** specify the Ethereum address that can claim the withdrawal (e.g. where you are withdrawing too) (<a name="1002-WITH-006" href="#1002-WITH-006">1002-WITH-006</a>)
- **should** be able to easily select an Ethereum key the app is already connected to
- **should** be able to withdraw to a different Ethereum key to the one the app is connected to
- **should** be warned if the input does not look like an ethereum address (wrong number of characters, not starting with 0x etc)
- if there is a withdraw delay on the selected asset:
- **should** see how large a withdrawal (or sum of withdrawals) needs to be to hit the `withdraw delay threshold`
- **should** see what the withdraw delay is in hours and mins (if hit)
- **should** see how much I have withdrawn in the last `withdraw delay period`
- **must** be warned if this withdraw will hit a the delay before hitting withdraw (<a name="1002-WITH-007" href="#1002-WITH-007">1002-WITH-007</a>)
- **must** be warned if there are known reasons that the prepared withdrawal will not work (<a name="1002-WITH-008" href="#1002-WITH-008">1002-WITH-008</a>)
- **must** submit a withdraw [vega transaction](0003-WTXN-submit_vega_transaction.md) (<a name="1002-WITH-009" href="#1002-WITH-009">1002-WITH-009</a>)
- if the preparing the withdraw on Vega fails:
- **must** be directed back to the withdraw form (containing the submitted values) and see an explanation of why the transaction failed, so I can fix and resubmit (<a name="1002-WITH-010" href="#1002-WITH-010">1002-WITH-010</a>)
- if the preparing the withdraw on Vega is successful:
- **must** see that withdraw is prepared (<a name="1002-WITH-011" href="#1002-WITH-011">1002-WITH-011</a>)
- if this withdraw will not hit the withdrawal threshold:
- **should** be prompted to complete the transaction on ethereum (see [complete ERC20 withdraw](#complete-erc20-withdraw-from-ethereum-bridge))
- **could** be directed to a list of incomplete withdrawals
- if this withdraw will hit withdrawal threshold:
- **must** see that the withdraw has been complete and is in the list waiting for the delay to pass (<a name="1002-WITH-024" href="#1002-WITH-024">1002-WITH-024</a>)
...so that I can get the details required to release my funds from the the Ethereum ERC20 bridge.
## Withdraws list / history
When looking to either complete a withdraw or view past withdraws, I...
- **must** be able to navigate to a list prepared withdrawals for the [connected to a vega wallet + key(s)](0002-WCON-connect_vega_wallet.md)
- for each prepared withdraw:
- **must** see the asset being withdrawn (<a name="1002-WITH-012" href="#1002-WITH-012">1002-WITH-012</a>)
- **must** see the [amount](9001-DATA-data_display.md#asset-balances) being withdrawn (<a name="1002-WITH-013" href="#1002-WITH-013">1002-WITH-013</a>)
- **must** see the destination of the withdrawal (e.g. Recipient Eth address) (<a name="1002-WITH-014" href="#1002-WITH-014">1002-WITH-014</a>)
- **should** see the date with withdraw was prepared
- **could** see the full signature bundle from Vega node (for use on Ethereum)
- for withdraws that are in progress:
- **must** see the status of the withdraw (e.g. pending) (<a name="1002-WITH-015" href="#1002-WITH-015">1002-WITH-015</a>)
- for completed withdraws:
- **could** see when it was completed on native chain (e.g. ethereum)
- **must** see a link to the transaction on native block explorer (e.g. etherscan) (<a name="1002-WITH-016" href="#1002-WITH-016">1002-WITH-016</a>)
- for withdraws that have not been completed on the external chain, but are not delayed (e.g. Ethereum):
- **must** see a link to complete the withdraw. See [complete ERC20 withdrawal](#complete-erc20-withdraw-from-ethereum-bridge) (<a name="1002-WITH-017" href="#1002-WITH-017">1002-WITH-017</a>)
- for withdrawals that have a delay in place before the transaction can be completed:
- **should** see much of the delay remains before it can be completed
- for withdraws that failed to be prepared (e.g. there was not enough in the general account):
- **must** show that the withdraw preparation failed (<a name="1002-WITH-025" href="#1002-WITH-025">1002-WITH-025</a>)
... so I can complete withdrawals or find details of previous ones
## Complete ERC20 withdraw from Ethereum bridge
When looking to submit the Ethereum transaction to release funds from the Vega bridge into my Ethereum wallet, I...
- **must** see a link to [connect an ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) if not already connected (<a name="1002-WITH-018" href="#1002-WITH-018">1002-WITH-018</a>)
- **must** see a link to [submit the ethereum transaction to finish withdrawal](0005-ETXN-submit_ethereum_transaction.md) (<a name="1002-WITH-019" href="#1002-WITH-019">1002-WITH-019</a>)
- **could** be warned if the connected ethereum wallet is different to the one that the withdraw is going to credit (this is permitted but is a good reminder to the user about what to expect)
- if successful:
- **must** see asset balances have been updated post withdrawal (<a name="1002-WITH-020" href="#1002-WITH-020">1002-WITH-020</a>)
- **must** see the list of withdrawals (with updated status) (<a name="1002-WITH-021" href="#1002-WITH-021">1002-WITH-021</a>)
- **could** see prompt to start another transaction or complete another incomplete one
- if failed:
- **must** see a description of why the transaction failed, and advised what to do (e.g. bad signature) (<a name="1002-WITH-022" href="#1002-WITH-022">1002-WITH-022</a>)
- **must** be returned to a state where I can correct anything that is wrong, and attempt to submit the transaction again (<a name="1002-WITH-023" href="#1002-WITH-023">1002-WITH-023</a>)
- **should** see a link to docs about withdrawals for trouble shooting (e.g. if the signer set has changed significantly since the withdraw was prepared)
- **should** see status of incomplete withdrawals (so I can confirm the withdraw I attempted to complete is incomplete)
... so the funds I withdrew from Vega are credited to my Ethereum key
+59
View File
@@ -0,0 +1,59 @@
# Transfer
## Transfer Window
- **Must** be able to open transfer window through transfer button under key, account history page and collateral options (<a name="1003-TRAN-001" href="#1003-TRAN-001">1003-TRAN-001</a>)
- **Must** be able to close the window with the x (<a name="1003-TRAN-002" href="#1003-TRAN-002">1003-TRAN-002</a>)
- **Must** display a message showing obfuscated key that funds will be transferred from (<a name="1003-TRAN-003" href="#1003-TRAN-003">1003-TRAN-003</a>)
- **Must** each field has their label. Vega key, Asset, Amount (<a name="1003-TRAN-004" href="#1003-TRAN-004">1003-TRAN-004</a>)
## Vega Key
- **Must**
if the user has multiple keys they must be able to swap between dropdown and manual entry (<a name="1003-TRAN-005" href="#1003-TRAN-005">1003-TRAN-005</a>)
- **Must**
if the user has multiple keys they must be able to select from their list of keys(<a name="1003-TRAN-006" href="#1003-TRAN-006">1003-TRAN-006</a>)
## Asset
- **Must** display a drop down with all assets in the portfolio (<a name="1003-TRAN-007" href="#1003-TRAN-007">1003-TRAN-007</a>)
- **Must** the holdings of each asset is displayed (<a name="1003-TRAN-008" href="#1003-TRAN-008">1003-TRAN-008</a>)
- **Must** i can select any available assets and selected asset is displayed (<a name="1003-TRAN-009" href="#1003-TRAN-009">1003-TRAN-009</a>)
- **Must** selected asset shortname is displayed in the amount field (<a name="1003-TRAN-010" href="#1003-TRAN-010">1003-TRAN-010</a>)
## Validation
- **Must** cannot choose amount over current collateral. Message is displayed (<a name="1003-TRAN-011" href="#1003-TRAN-011">1003-TRAN-011</a>)
- **Must** display "required" message on each field if left blank when clicking button "Confirm Transfer" (<a name="1003-TRAN-012" href="#1003-TRAN-012">1003-TRAN-012</a>)
- **Must** display "Invalid vega key" message on Vega Key field if entered key doesn't pass validation(<a name="1003-TRAN-013" href="#1003-TRAN-013">1003-TRAN-013</a>)
- **Must** "Value below minimum" message is shown if amount is lower than minimum(<a name="1003-TRAN-014" href="#1003-TRAN-014">1003-TRAN-014</a>)
## Transfer
- **Must** can select include transfer fee (<a name="1003-TRAN-015" href="#1003-TRAN-015">1003-TRAN-015</a>)
- **Must** display tooltip for "Include transfer fee" when hovered over.(<a name="1003-TRAN-016" href="#1003-TRAN-016">1003-TRAN-016</a>)
- **Must** display tooltip for "Transfer fee when hovered over.(<a name="1003-TRAN-017" href="#1003-TRAN-017">1003-TRAN-017</a>)
- **Must** display tooltip for "Amount to be transferred" when hovered over.(<a name="1003-TRAN-018" href="#1003-TRAN-018">1003-TRAN-018</a>)
- **Must** display tooltip for "Total amount (with fee)" when hovered over.(<a name="1003-TRAN-019" href="#1003-TRAN-019">1003-TRAN-019</a>)
- **Must** amount to be transferred and transfer fee update correctly when include transfer fee is selected (<a name="1003-TRAN-020" href="#1003-TRAN-020">1003-TRAN-020</a>)
- **Must** total amount with fee is correct with and without "Include transfer fee" selected (<a name="1003-TRAN-021" href="#1003-TRAN-021">1003-TRAN-021</a>)
- **Must** i cannot select include transfer fee unless amount is entered (<a name="1003-TRAN-022" href="#1003-TRAN-022">1003-TRAN-022</a>)
- **Must** With all fields entered correctly, clicking "confirm transfer" button will start transaction(<a name="1003-TRAN-023" href="#1003-TRAN-023">1003-TRAN-023</a>)
+70
View File
@@ -0,0 +1,70 @@
# Associate and disassociate governance tokens with a Vega key
The Governance token on a Vega network is an ERC20 ethereum token. It has two utilities on Vega,
- Staking the Proof of stake network,
- Participating in Governance.
To use the Governance token on a Vega network it first needs to be associated with a Vega key/party. This Vega key can then stake, propose and vote.
The word "associate" is used in some user interfaces, as apposed the word "stake" in function names. Stake can be avoided to prevent users thinking they would get a return only after the staking step. On Vega `Staking = Association + Nomination`, as in you need to run the "stake " function on Ethereum but then the nominate step on Vega before you get staking income. See [Glossary](../glossaries/staking-and-governance.md).
Associated tokens also count as vote weight in on-Vega governance (new markets etc), A parties vote weight is backed by the number of Governance tokens associated with that Vega key/party.
Associating tokens to a Vega key is a little like depositing, except, a deposit can only be released by the Vega network, where as an association can be revoked on ethereum by the the eth key that did the association.
Governance tokens may be held by a [Vesting contract](1005-VEST-vesting.md).
## Token discovery
When looking to acquire governance tokens, I...
- **must** see the contract address for the governance token of the Vega network <a name="1004-ASSO-001" href="#1004-ASSO-001">1004-ASSO-001</a>
...so I can participate in governance and staking.
## Associate
When looking to stake validators or participate in governance, I first need to associate governance tokens with a Vega wallet/key, I...
- **must** [connect an Ethereum wallet/key](0004-EWAL-connect_ethereum_wallet.md) to see tokens it may have in wallet or attributed to it in the vesting contract <a name="1004-ASSO-002" href="#1004-ASSO-002">1004-ASSO-002</a>
- **must** select a Vega key to associate to <a name="1004-ASSO-003" href="#1004-ASSO-003">1004-ASSO-003</a>
- **must** be able use a [connected Vega wallet](0002-WCON-connect_vega_wallet.md) as instead of manually inputting a public key <a name="1004-ASSO-004" href="#1004-ASSO-004">1004-ASSO-004</a>
- **should** be able to populate field with a string, so I can associate to a wallet I without connecting it
- if the connected ethereum wallet has vesting tokens: **must** be able to select to associate from either the vesting contract or the wallet (The default should be wallet and the option to use vesting tokens should only appear if there are tokens in a tranche (associated or not)) <a name="1004-ASSO-006" href="#1004-ASSO-006">1004-ASSO-006</a>
- **must** see the number of associated and un-associated tokens in the selected wallet/vesting contract <a name="1004-ASSO-007" href="#1004-ASSO-007">1004-ASSO-007</a>
- **must** select the amount of tokens to associate <a name="1004-ASSO-008" href="#1004-ASSO-008">1004-ASSO-008</a>
- **must** be able to populate the input with the amount of un-associated tokens for the selected wallet/vesting contract <a name="1004-ASSO-009" href="#1004-ASSO-009">1004-ASSO-009</a>
- **must** be warned if the amount being associated is greater than the amount available in the connected ethereum wallet <a name="1004-ASSO-010" href="#1004-ASSO-010">1004-ASSO-010</a>
- **must** submit the association on [Ethereum transaction(s) inc ERC20 approval if required](0005-ETXN-submit_ethereum_transaction.md) <a name="1004-ASSO-011" href="#1004-ASSO-011">1004-ASSO-011</a>
- **must** see feedback whether my association has been registered on Ethereum <a name="1004-ASSO-012" href="#1004-ASSO-012">1004-ASSO-012</a>
- **must** see feedback that the association has been registered by Vega and that it can be used after the number of Ethereum block confirmations required (typically 50, check network param) <a name="1004-ASSO-013" href="#1004-ASSO-013">1004-ASSO-013</a>
- **should** be able to see a balance for the number of tokens associated that are ready for use <a name="1004-ASSO-014" href="#1004-ASSO-014">1004-ASSO-014</a>
- **should** be able to see a balance for the number of tokens for each pending association <a name="1004-ASSO-015" href="#1004-ASSO-015">1004-ASSO-015</a>
- on completion: **should** be prompted to go on to [nominate](2001-STKE-staking.md) and/or participate in [Governance](1004-GOVE-governance_list.md)
- **must** see the balances of tokens available in Ethereum wallet updated (both associated and un-associated) <a name="1004-ASSO-030" href="#1004-ASSO-030">1004-ASSO-030</a>
- **must** see the balances of tokens available in vesting contract updated (both associated and un-associated) <a name="1004-ASSO-032" href="#1004-ASSO-032">1004-ASSO-032</a>
...so I can then use the Vega wallet to use my tokens.
## Disassociate
When wanting to remove governance tokens, I...
- **must** [connect an Ethereum wallet/key](0004-EWAL-connect_ethereum_wallet.md) to see tokens it may have in wallet or attributed to it in the vesting contract <a name="1004-ASSO-018" href="#1004-ASSO-018">1004-ASSO-018</a>
- **must** see a list Vega keys that the connected Ethereum wallet has associated too <a name="1004-ASSO-019" href="#1004-ASSO-019">1004-ASSO-019</a>
- **must** see an amount associated with each key <a name="1004-ASSO-020" href="#1004-ASSO-020">1004-ASSO-020</a>
- **must** see the full Vega public key associated too <a name="1004-ASSO-021" href="#1004-ASSO-021">1004-ASSO-021</a>
- **must** see the the origin of the association: wallet or vesting contract <a name="1004-ASSO-022" href="#1004-ASSO-022">1004-ASSO-022</a>
- **should** be able to select one row (vega key + amount) to populate disassociate form with the key you want to disassociate from and how much <a name="1004-ASSO-023" href="#1004-ASSO-023">1004-ASSO-023</a>
- If some of the tokens for the given Eth key are held by the vesting contract: **must** select to return tokens to Vesting contract <a name="1004-ASSO-024" href="#1004-ASSO-024">1004-ASSO-024</a>
- **must** select an amount of tokens to disassociate <a name="1004-ASSO-031" href="#1004-ASSO-031">1004-ASSO-031</a>
- **must** be able to populate the input with the amount of associated tokens for the selected Vega wallet/contract <a name="1004-ASSO-025" href="#1004-ASSO-025">1004-ASSO-025</a>
- **should** be warned that disassociating will forfeit and rewards for the current epoch and reduce the Vote weigh on any open proposals
- **must** be warned if the inputs on the form will result in an invalid withdraw, before submitting <a name="1004-ASSO-026" href="#1004-ASSO-026">1004-ASSO-026</a>
- **must** action the disassociation [Ethereum transaction](0005-ETXN-submit_ethereum_transaction.md) <a name="1004-ASSO-027" href="#1004-ASSO-027">1004-ASSO-027</a>
- **must** see feedback on the progress of the disassociation on ethereum <a name="1004-ASSO-028" href="#1004-ASSO-028">1004-ASSO-028</a>
- **must** see new associated balances in Vega (theses should be applied instantly, rather than wait for the 50 eth blocks like associate) <a name="1004-ASSO-029" href="#1004-ASSO-029">1004-ASSO-029</a>
- on completion (if tokens were returned to vesting contract): **could** be prompted to go on to [redeem](1001-VEST-vesting.md).
...so that I can transfer them to another Ethereum wallet (e.g. sell them on an exchange).
+80
View File
@@ -0,0 +1,80 @@
# Vesting
Some governance tokens may be held by a Vesting contract. This means that can be "owned" by an Ethereum key but not freely transferred until a vesting terms are complete.
## list of tranches
When looking to understand to overall vesting schedule for tokens, I...
- **must** see a list of tranches <a name="1005-VEST-001" href="#1005-VEST-001">1005-VEST-001</a>
- **should** see a visualization of the vesting schedule with a break down of the type of token holders (e.g. team, investors, community) <a name="1005-VEST-002" href="#1005-VEST-002">1005-VEST-002</a>
For each tranche:
- **must** see a tranche number <a name="1005-VEST-003" href="#1005-VEST-003">1005-VEST-003</a>
- **could** see any annotation of what this tranche is about (e.g. community schedule A)
- **must** see a sum of tokens in the tranche <a name="1005-VEST-005" href="#1005-VEST-005">1005-VEST-005</a>
- **must** see how many tokens in the tranche are locked <a name="1005-VEST-006" href="#1005-VEST-006">1005-VEST-006</a>
- **must** see how how many tokens in the tranche are redeemable <a name="1005-VEST-007" href="#1005-VEST-007">1005-VEST-007</a>
- **must** see the vesting terms for each tranche (when unlocking stats and ends) <a name="1005-VEST-008" href="#1005-VEST-008">1005-VEST-008</a>
... so I can understand how circulating supply could change over time.
## Details of a tranche
When looking into a specific tranche, I...
- **must** see all the same details as the [list of tranches](#details-of-a-tranche)
- **should** see a list of ethereum wallets with tokens in this tranche
for each ethereum wallet:
- **should** see the full eth address of the wallet
- **should** see the total tokens this address holds in this tranche
- **should** see how many tokens in the tranche are locked
- **should** see how how many tokens in the tranche are redeemable
... so I can see the details of how tokens are distributed in this tranche
## See summary for a given Ethereum key
When looking to see how many tokens I have in total, and how many I might be able to redeem, I...
- **must** be able to [Connect and ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) <a name="1005-VEST-018" href="#1005-VEST-018">1005-VEST-018</a>
- **should** be able input an ethereum address
for the a given Ethereum wallet/address/key:
- **must** see a total of tokens across all tranches <a name="1005-VEST-020" href="#1005-VEST-020">1005-VEST-020</a>
- **must** see how many tokens across all tranches are locked <a name="1005-VEST-021" href="#1005-VEST-021">1005-VEST-021</a>
- **must** see how many tokens across all tranches are redeemable <a name="1005-VEST-022" href="#1005-VEST-022">1005-VEST-022</a>
- **must** see a list of tranches this key has tokens in <a name="1005-VEST-023" href="#1005-VEST-023">1005-VEST-023</a>
- **must** see a total of tokens in each tranche <a name="1005-VEST-024" href="#1005-VEST-024">1005-VEST-024</a>
- **must** see how many tokens in each tranche are locked <a name="1005-VEST-025" href="#1005-VEST-025">1005-VEST-025</a>
- **must** see how many tokens in each tranche are redeemable <a name="1005-VEST-026" href="#1005-VEST-026">1005-VEST-026</a>
- **must** see an option to redeem from tranche <a name="1005-VEST-027" href="#1005-VEST-027">1005-VEST-027</a>
- **must** be warned if amount that can be redeemed from that tranche is greater than the un-associated balance for that Eth key (because this will cause the redeem function to fail) <a name="1005-VEST-028" href="#1005-VEST-028">1005-VEST-028</a>
- **should** see how many tokens I'd need to disassociate to be able to run the redeem function (this should be rounded up to avoid the transaction failing due to more tokens having unlocked since the user looked at the form)
- **should** see link to [disassociate](1004-ASSO-associate.md)
... so I can easily see how many tokens I have, and can redeem.
## Redeem tokens from a tranche
Note: it is not possible to choose how many tokens you redeem from a tranche, instead you select a tranche and the smart contract will attempt to redeem all. However, it will fail if some of the amount it attempts to redeem have been associated to a Vega key. Therefore the job of this page is to help the user work out how many tokens to disassociate before they can successfully redeem.
When looking to redeem tokens, I...
- **must** [connect the ethereum wallet](0004-EWAL-connect_ethereum_wallet.md) that holds tokens <a name="1005-VEST-029" href="#1005-VEST-029">1005-VEST-029</a>
- must see see all tranches that you have tokens in (including tranche 0) <a name="1005-VEST-036" href="#1005-VEST-036">1005-VEST-036</a>
- must see a total of tokens across all tranches (including tranche 0) <a name="1005-VEST-037" href="#1005-VEST-037">1005-VEST-037</a>
- **must** select a tranche to redeem from <a name="1005-VEST-030" href="#1005-VEST-030">1005-VEST-030</a>
- **must** see the number of tokens that can be redeemed <a name="1005-VEST-031" href="#1005-VEST-031">1005-VEST-031</a>
- **must** be warned if the number of tokens you would be attempting to redeem is greater than you have unassociated <a name="1005-VEST-035" href="#1005-VEST-035">1005-VEST-035</a>
- **should** tell you how many tokens to disassociate for the redeem function to work (should round up to create a buffer for the tokens that may unlock between now and when the user gets to the disassociate form)
- **should** see a link to disassociate
- **must** submit the redeem from tranche [ethereum transaction](0005-ETXN-submit_ethereum_transaction.md) <a name="1005-VEST-032" href="#1005-VEST-032">1005-VEST-032</a>
- **must** get feedback on the progress of the Ethereum transaction <a name="1005-VEST-033" href="#1005-VEST-033">1005-VEST-033</a>
- **must** see updated balances (in the trance and my eth wallet) after redemption <a name="1005-VEST-034" href="#1005-VEST-034">1005-VEST-034</a>
... so that I can use this tokens more generally on Ethereum (transfer to another key etc)
+121
View File
@@ -0,0 +1,121 @@
# Staking
Staking is the act of securing a Vega network by nominating good validators with the [governance token](../protocol/0071-STAK-erc20_governance_token_staking.md). Staking is rewarded with a share of trading fees (and [treasury rewards](../0056-REWA-rewards_overview.md)). See the [glossary](../glossaries/staking-and-governance.md) and [these specs](../protocol#delegation-staking-and-rewards) for more on staking.
When staking a user may be motivated to select validators to maximize the rewards they get for the tokens they hold, this means selecting validator(s) who are less likely to be penalized (e.g. over staked, poor performance). Users may wish to stake more than one validator to diversify. Users will want/need to manage their stake over time to ensure they are getting a good return, e.g. move stake between validators. Staking is also important for facilitating protocol upgrades.
## Understand staking on Vega
When considering whether to stake on Vega, I...
- **should** see information to help inform me what return I might expect from staking (other protocols might show a typical APY)
- **must** see that the governance token is an ethereum ERC20 token and needs to attributed (or associated) to a Vega wallet for use on Vega <a name="1002-STKE-002" href="#1002-STKE-002">1002-STKE-002</a>
- **must** see detailed documentation on how staking works on Vega <a name="1002-STKE-003" href="#1002-STKE-003">1002-STKE-003</a>
...so I can decide if I want to stake on Vega, and how to go about doing it.
Note: There are many ways that "understanding the return" can be done, and this does not impose a particular solution. Solutions could...
- look at previous epochs,
- average this over a period,
- select a range of validators or just one,
- could have a calculator that allows the user to enter some values or just show this on the list of validators
Note: Income may come in a range of different tokens, as markets can settle in different assets, and there may be rewards paid out by the treasury.
## Associate tokens
Before I stake, I need to [Associate tokens](./1000-ASSO-associate.md) with a Vega wallet/key...
- **must** see link[Associate tokens](./1000-ASSO-associate.md)
- **should** see that if no further action is taken, newly associated tokens will be nominated to validators based on existing distribution
...so that I can nominate validators.
## Select validator(s)
When selecting what validators to nominate with my stake, I...
- **should** be able to select any data that is available on Validators in a table
- **must** see all validator information without having to connect Vega wallet <a name="1002-STKE-050" href="#1002-STKE-050">1002-STKE-050</a>
- see "static" information about the validator (these can change, just not frequently)
- **must** see name <a name="1002-STKE-006" href="#1002-STKE-006">1002-STKE-006</a>
- **must** see Vega public key <a name="1002-STKE-008" href="#1002-STKE-008">1002-STKE-008</a>
- **should** see a URL where to find more information about the validator (if there is one)
- **must** see Ethereum address<a name="1002-STKE-010" href="#1002-STKE-010">1002-STKE-010</a>
- can see data for the current/next epoch, for each validator
- **must** see the current "status" (consensus, Ersatz, New etc) <a name="1002-STKE-011" href="#1002-STKE-011">1002-STKE-011</a>
- **must** see a total stake (inc self stake) <a name="1002-STKE-012" href="#1002-STKE-012">1002-STKE-012</a>
- **must** see self stake <a name="1002-STKE-013" href="#1002-STKE-013">1002-STKE-013</a>
- **must** see nominated stake <a name="1002-STKE-014" href="#1002-STKE-014">1002-STKE-014</a>
- **must** see total stake as a % of total staked across all nodes <a name="1002-STKE-051" href="#1002-STKE-051">1002-STKE-051</a>
- **must** see total stake change next epoch <a name="1002-STKE-015" href="#1002-STKE-015">1002-STKE-015</a>
- **must** see self stake <a name="1002-STKE-016" href="#1002-STKE-016">1002-STKE-016</a>
- **must** see nominated stake <a name="1002-STKE-017" href="#1002-STKE-017">1002-STKE-017</a>
- **must** see total stake as a % change <a name="1002-STKE-052" href="#1002-STKE-052">1002-STKE-052</a>
- **should** see the version of Vega they are currently running
- **should** see the version of Vega they propose running
- can see data for the previous epoch
- **must** see the overall "score" for a validator for the previous epoch <a name="1002-STKE-020" href="#1002-STKE-020">1002-STKE-020</a>
- can see all the inputs to that "score"
- **must** see Ranking score <a name="1002-STKE-021" href="#1002-STKE-021">1002-STKE-021</a>
- **must** see stake score <a name="1002-STKE-022" href="#1002-STKE-022">1002-STKE-022</a>
- **must** see performance score <a name="1002-STKE-023" href="#1002-STKE-023">1002-STKE-023</a>
- **must** see voting score <a name="1002-STKE-024" href="#1002-STKE-024">1002-STKE-024</a>
- can see data for previous epochs
- **should** see the the overall "score" for all previous epochs for each validator <a name="1002-STKE-025" href="#1002-STKE-025">1002-STKE-025</a>
- can see a breakdown of all the inputs to that "score" for all previous epochs
- **should** see Ranking score
- **should** see stake score
- **should** see performance score
- **should** see voting score
...so I can select validators that should give me the biggest return.
## Nominate a validator
Note: User interfaces may use the term "Nominate", technically the function is called "delegate". Delegating tokens to a validator may imply that you also give that validator your vote on proposals, at time of writing, it does not. It only gives them the potential for more "voting power" in the production of blocks.
Within a staking epoch (typically 24 hours) a user can change their nominations many times, however the changes are only effective at the end of the epoch. You will only get rewards for a full epoch staked.
When attributing some (or all of my governance tokens to a given validator), I...
- **must** select a validator I want to nominate <a name="1002-STKE-031" href="#1002-STKE-031">1002-STKE-031</a>
- **must** see link to [connect to a Vega wallet/key](0002-WCON-connect_vega_wallet.md) (if not already) that has associated Vega (or Pending association) <a name="1002-STKE-032" href="#1002-STKE-032">1002-STKE-032</a>
- **must** select an amount of tokens <a name="1002-STKE-033" href="#1002-STKE-033">1002-STKE-033</a>
- **must** be able to populate this with the amount of governance tokens that will be associated but not nominated at the beginning of the next epoch <a name="1002-STKE-034" href="#1002-STKE-034">1002-STKE-034</a>
- **must** be warned if the amount I am about to nominate is below a minimum amount (spam protection) <a name="1002-STKE-035" href="#1002-STKE-035">1002-STKE-035</a>
- **must** be warned if the amount I am about to nominate is more than I have associated + un-nominated at the end of current epoch/beginning of next <a name="1002-STKE-036" href="#1002-STKE-036">1002-STKE-036</a>
- **must** submit the nomination [Vega transactions](0003-WTXN-submit_vega_transaction.md) <a name="1002-STKE-037" href="#1002-STKE-037">1002-STKE-037</a>
- **must** see feedback that my nomination has been registered, and will be processed at the next epoch <a name="1002-STKE-038" href="#1002-STKE-038">1002-STKE-038</a>
- **must** see all my pending nomination changes for the next epoch <a name="1002-STKE-039" href="#1002-STKE-039">1002-STKE-039</a>
...so that I am rewarded for a share based on this validators performance.
## Monitor staking rewards
When checking if im getting the staking return that I was expecting, I...
- See [Staking income](2002-SINC-staking-income.md)
...so that I can make decisions about my staking, e.g. whether to re-distribute my stake.
## Un-nominate validator
When removing stake from a validator, I...
- **must** select a validator I want to un-nominate <a name="1002-STKE-040" href="#1002-STKE-040">1002-STKE-040</a>
- **must** [connect to a Vega wallet/key](0002-WCON-connect_vega_wallet.md) with nominated stake, if not already <a name="1002-STKE-041" href="#1002-STKE-041">1002-STKE-041</a>
- - **must** have the option of withdrawing nominated amount at the end of the epoch (and maintain the staking income for the current epoch) <a name="1002-STKE-053" href="#1002-STKE-053">1002-STKE-053</a>
- **should** have the option of withdrawing nomination amount now immediately (and forfeit the staking income)
- **must** set an amount to remove from a validator <a name="1002-STKE-044" href="#1002-STKE-044">1002-STKE-044</a>
- **must** be able populate with the total delegated amount at the time where un-nominate will happen <a name="1002-STKE-045" href="#1002-STKE-045">1002-STKE-045</a>
- **must** be warned if amount is greater than the amount that will be on that validator at the end of the epoch <a name="1002-STKE-046" href="#1002-STKE-046">1002-STKE-046</a>
<a name="1002-STKE-047" href="#1002-STKE-047">1002-STKE-047</a>
- **must** submit un-nominate [Vega transaction](0003-WTXN-submit_vega_transaction.md) <a name="1002-STKE-048" href="#1002-STKE-048">1002-STKE-048</a>
- **must** see feedback that the un-nomination has been registered, and that the un-nominated amount is now available for re-nomination <a name="1002-STKE-049" href="#1002-STKE-049">1002-STKE-049</a>
... so that I can use this stake for another validator etc.
note: if a user just wishes to seel their tokens and not wait for the end of an epoch they could simply [disassociate](1004-ASSO-associate.md#disassociate) (as long as tokens are not held by vesting contract)
+31
View File
@@ -0,0 +1,31 @@
# Staking income
## Monitor staking rewards
When checking the staking rewards, I...
- must be [connected to a Vega wallet](0002-WCON-connect_vega_wallet.md)
- **must** see when the current epoch ends <a name="2002-SINC-001" href="#2002-SINC-001">2002-SINC-001</a>
- **should** see when the current epoch started
- **should** see how much the connected wallet might earn at the end of the epoch (with some assumptions)
- **should** see the balance of all infrastructure fee accounts for the current epoch
- **should** see real time validator score for the current epoch
- **should** see if any validators have had a penalty this epoch
- **must** see the sum of Tokens I have nominated to each validator in the current epoch <a name="2002-SINC-007" href="#2002-SINC-007">2002-SINC-007</a>
- **should** see what percentage of my Tokens I have nominated to each validator in the current epoch
- **must** see the staking income I have received for each epoch previous <a name="2002-SINC-009" href="#2002-SINC-009">2002-SINC-009</a>
- **must** see asset name <a name="2002-SINC-010" href="#2002-SINC-010">2002-SINC-010</a>
- **must** see balance of asset <a name="2002-SINC-011" href="#2002-SINC-011">2002-SINC-011</a>
- **must** see type (organic or treasury) <a name="2002-SINC-012" href="#2002-SINC-012">2002-SINC-012</a>
- **should** see the staking income by epoch, broken down by validator
- **should** see the staking income by epoch, broken down by market
- **should** see all income values expressed in a single currency
- **must** see current asset balances for connected wallets <a name="2002-SINC-016" href="#2002-SINC-016">2002-SINC-016</a>
- **must** see link to [withdraw](1002-WITH-withdraw.md) assets <a name="2002-SINC-017" href="#2002-SINC-017">2002-SINC-017</a>
- **must** see where I can see where I did not receive full income because the validator suffered penalties <a name="2002-SINC-018" href="#2002-SINC-018">2002-SINC-018</a>
...so that I can make decisions about my staking
+174
View File
@@ -0,0 +1,174 @@
# Vote
Background: [Governance spec](../protocol/0028-GOVE-governance.md)
and [docs](https://s.vega.xyz/s/mainnet/concepts/vega-protocol#governance).
There are a few things that can be governed on Vega...
- Network parameters (variables used by the network)
- Markets (creation and changes to existing)
- Assets (creation on changes to existing)
- "Freeform", which has no affect on the network but can be used to to measure token holders views
To make proposal: a user will require an amount of the Governance token [associated](1004-ASSO-associate.md) with their key.
To vote: a user will require [associated](1004-ASSO-associate.md) Governance tokens (or in the case of market change proposals they could have an active liquidity provision).
Each vote has a weight behind it based on the number of associate tokens or the liquidity provision's equity like share at the point in time that the vote closes.
Each proposal type will have a majority required (vote weight in favour) and a participation required (the turn out for the vote needs to be sufficient to be valid).
A proposal has a life cycle and various statuses to track it's progress. proposals that accept votes can have a few different statuses. A short hand is used in these ACs:
- Open = Accepting votes (includes `waitingForNodeVote`)
- To enact = passed but not yet enacted (`pending)
- Closed = was accepting votes but deadline has passed (e.g. `passed`, `rejected` etc)
- `Failed` = did not get to the point of accepting votes.
## list of proposals
When looking for a particular proposal or wanting to see what proposals are open, soon to enact or closed, I...
- **must** see link to details on how governance works in docs (<a name="3001-VOTE-001" href="#3001-VOTE-001">3001-VOTE-001</a>)
- **must** see link(s) to make proposals (<a name="3001-VOTE-002" href="#3001-VOTE-002">3001-VOTE-002</a>)
- **must** if there are no proposals, see that there have been no proposals since the last chain checkpoint restore (<a name="3001-VOTE-003" href="#3001-VOTE-003">3001-VOTE-003</a>)
- **must** see open proposals (and ones due for enactment) distinct from others (e.g grouped by "open", "to enact" "closed") (note: freeform proposals do not enact so should be shown as "closed" when "passed") (<a name="3001-VOTE-004" href="#3001-VOTE-004">3001-VOTE-004</a>)
- **should** see proposals sorted with the ones closest to enactment first (within each group)
- **must** see a history of all "closed" proposals (<a name="3001-VOTE-006" href="#3001-VOTE-006">3001-VOTE-006</a>)
- can search for a proposal by:
- **should** be able to search by proposal ID
- **should** be able to search by public key of the proposer
- **should** be abel to search by market ID/name/code (ID may be the same as proposal ID)
- **should** be able to search by asset name/symbol
- **should** be able to search by network parameter
- **should** be able to search by content of proposal description
- **must** see how long is left to vote (<a name="3001-VOTE-018" href="#3001-VOTE-018">3001-VOTE-018</a>)
- **must** see colourful / shouty things that prompt me to vote urgently on things "CLOSING SOON" (<a name="3001-VOTE-019" href="#3001-VOTE-019">3001-VOTE-019</a>)
- **must** see the current voting status and whether it will pass (<a name="3001-VOTE-020" href="#3001-VOTE-020">3001-VOTE-020</a>)
- **must** see the current participation and whether it will pass (<a name="3001-VOTE-021" href="#3001-VOTE-021">3001-VOTE-021</a>)
for each proposal:
- **must** see the proposal ID (<a name="3001-VOTE-008" href="#3001-VOTE-008">3001-VOTE-008</a>)
- **must** see who the proposer is (e.g. their public address) (<a name="3001-VOTE-009" href="#3001-VOTE-009">3001-VOTE-009</a>)
- **must** see the type of proposal (<a name="3001-VOTE-007" href="#3001-VOTE-007">3001-VOTE-007</a>)
- **must** see the proposal title (<a name="3001-VOTE-097" href="#3001-VOTE-097">3001-VOTE-097</a>)
- **should** see a summary of what the type of proposed change is, without looking at details (network, new market etc)
- for network parameters: **should** see what parameter is being changed and new value
- for network parameters: **could** see what the current values are for that parameter
- for network parameters: **could** see if there are other open proposals for the same parameter
- for new markets: **should** see the type of market (e.g. Future)
- for new markets: **could** see the type trading mode of the market (e.g. auction, continuous)
- for new markets: **should** see the name of the new market
- for new markets: **should** see the code of the new market
- for new markets: **should** see the settlement asset of the new market (not just asset ID but asset Symbol)
- for new markets: **could** see a summary of the oracle used for settlement
- for market changes: **should** see the name of the market being changed
- for market changes: **should** see a summary of what parameters are being changed
- for market changes: **should** see a the proposed values for parameters
- for market changes: **should** see a the current values for that parameter
- for market changes: **could** see if there are other open proposals for the same market
- for new assets: **must** see the name of the new asset (<a name="3001-VOTE-026" href="#3001-VOTE-026">3001-VOTE-026</a>)
- for new assets: **must** see the code of the new asset (<a name="3001-VOTE-027" href="#3001-VOTE-027">3001-VOTE-027</a>)
- for new assets: **must** see the source of the new asset (e.g. ERC20) (<a name="3001-VOTE-028" href="#3001-VOTE-028">3001-VOTE-028</a>)
- for new assets (if source is ERC20): **must** see contract address (<a name="3001-VOTE-095" href="#3001-VOTE-095">3001-VOTE-095</a>)
- for new assets (if source is ERC20): **must** see if the Asset has been whitelisted on the bridge (<a name="3001-VOTE-096" href="#3001-VOTE-096">3001-VOTE-096</a>)
- for asset changes: **must** see name of asset being changed (<a name="3001-VOTE-029" href="#3001-VOTE-029">3001-VOTE-029</a>)
- for asset changes: **must** see the parameter(s) being changed (<a name="3001-VOTE-030" href="#3001-VOTE-030">3001-VOTE-030</a>)
- for asset changes; **must** see the new value for the parameters being changed (<a name="3001-VOTE-031" href="#3001-VOTE-031">3001-VOTE-031</a>)
- for asset changes: **could** see if there are other open proposals for the same parameter(s)
- for asset changes: **should** see the current values for these parameters
- for freeform: **must** see a summary of the proposal (suggest the first x characters of the proposal blob)
- **must** see the proposal status e.g. passed, open, waiting for node to vote) (<a name="3001-VOTE-035" href="#3001-VOTE-035">3001-VOTE-035</a>)
- for new asset proposals: **must** see if an asset has not yet been whitelisted on the bridge (<a name="3001-VOTE-036"
href="#3001-VOTE-036">3001-VOTE-036</a>)
- **must** see the proposal's terms in JSON
(<a name="3001-VOTE-010" href="#3001-VOTE-010">3001-VOTE-010</a>)
- for open proposals: **must** see a summary of how the vote count stands and if it looks like proposal will pass or not (note some of these are repeated in more details in the [details section](#details-of-a-proposal)) (<a name="3001-VOTE-037" href="#3001-VOTE-037">3001-VOTE-037</a>)
- if the proposal failed (had the status of "failed", because it was an invalid on submission) they **should not** appear in the list (instead the proposer will see this after submission)
- if the proposal looks like it will fail due to insufficient participation: **should** see "participation not reached"
- else if: proposal looks like it might fail due to insufficient majority (and is not a market change proposal): **should** see "Majority not reached"
- else if (is a market change proposal) and is likely to pass because of liquidity providers vote: **should** see "set to pass by Liquidity provider vote"
- else if: proposal is likely to pass: **should** see "set to pass"
- **must** see when (date/time) voting closes on proposal (<a name="3001-VOTE-043" href="#3001-VOTE-043">3001-VOTE-043</a>)
- for (non-freefrom) proposals that have passed but not enacted: **must** see when they will enact (<a name="3001-VOTE-044" href="#3001-VOTE-044">3001-VOTE-044</a>)
- for (non-freefrom) proposals that have passed but not enacted: **should** see when (date/time) voting closed
- for (non-freeform) proposals that enacted: **must** see when they enacted (<a name="3001-VOTE-046" href="#3001-VOTE-046">3001-VOTE-046</a>)
- for freeform proposals that have passed: **must** see when they passed (<a name="3001-VOTE-047" href="#3001-VOTE-047">3001-VOTE-047</a>)
- for proposals that did not pass due to lack of participation: **must** see "Participation not reached" (<a name="3001-VOTE-048" href="#3001-VOTE-048">3001-VOTE-048</a>)
- for proposals that did not pass due to lack of majority: **must** see "Majority not reached" (<a name="3001-VOTE-049" href="#3001-VOTE-049">3001-VOTE-049</a>)
- for proposals that did not pass due to failure: **must** see "Failed" (<a name="3001-VOTE-050" href="#3001-VOTE-050">3001-VOTE-050</a>)
- for proposals that I ([connected Vega](./0002-WCON-connect_vega_wallet.md) key) have voted on: **should** see my vote (for or against)
...so I can see select one to view and vote, or view outcome.
## Details of a proposal
When looking at a particular proposal, I...
- see [the same details in the list of proposals](#list-of-proposals) and:
- **must** have option to see raw JSON of proposal (<a name="3001-VOTE-052" href="#3001-VOTE-052">3001-VOTE-052</a>)
- **should** display the proposed change details displayed in a human readable format (e.g. with market id, shown along with that market name)
- **must** see the rationale title (<a name="3001-VOTE-054" href="#3001-VOTE-054">3001-VOTE-054</a>)
- **must** see the full rationale description if there is one (<a name="3001-VOTE-055" href="#3001-VOTE-055">3001-VOTE-055</a>)
- **must** see rationale description rendered with markdown (<a name="3001-VOTE-101" href="#3001-VOTE-101">3001-VOTE-101</a>)
For open proposals:
- **must** show a summary of vote status (base on the current total amount associated tokens, note this could change before the vote ends) (<a name="3001-VOTE-057" href="#3001-VOTE-057">3001-VOTE-057</a>)
- **must** see if the token vote has met a required participation threshold (<a name="3001-VOTE-058" href="#3001-VOTE-058">3001-VOTE-058</a>)
- **must** see the sum of tokens that have voted so far (<a name="3001-VOTE-059" href="#3001-VOTE-059">3001-VOTE-059</a>)
- **must** see sum of tokens that have voted as a percentage of total voted (<a name="3001-VOTE-011" href="#3001-VOTE-011">3001-VOTE-011</a>)
- **should** see what the participation threshold is for this proposal (note this is set per proposal once the proposal hits the chain based on the current network params, incase a proposal is set to enact that changes threshold)
- **must** see if the Token vote has met the required majority threshold (<a name="3001-VOTE-062" href="#3001-VOTE-062">3001-VOTE-062</a>)
- **must** see the sum of tokens that have voted in favour of the proposal (<a name="3001-VOTE-064" href="#3001-VOTE-064">3001-VOTE-064</a>)
- **should** see sum of tokens that have votes in favour of proposal as percentage of total associated
- **should** see what the majority threshold is for this proposal (note this is see per proposal, incase a proposal is set to enact that changes threshold)
For open market change proposals, all of the above and:
- **must** show a summary of vote status (base on the current equality like share, note this could change before the vote ends) (<a name="3001-VOTE-067" href="#3001-VOTE-067">3001-VOTE-067</a>)
- **must** see if the equity like share vote has met the required participation threshold (<a name="3001-VOTE-068" href="#3001-VOTE-068">3001-VOTE-068</a>)
- **must** see the equity like share % that has voted so far (<a name="3001-VOTE-069" href="#3001-VOTE-069">3001-VOTE-069</a>)
- **should** see what the equity like share threshold is for this proposal (note this is see per proposal, incase a proposal is set to enact that changes threshold)
- **must** see if the equity like share vote has met the required majority threshold (<a name="3001-VOTE-071" href="#3001-VOTE-071">3001-VOTE-071</a>)
- **must** see the equity like share as percentage that has voted in favour of the proposal (<a name="3001-VOTE-072" href="#3001-VOTE-072">3001-VOTE-072</a>)
- **must** see what the majority threshold is for this proposal (note this is see per proposal, incase a proposal is set to enact that changes threshold) (<a name="3001-VOTE-073" href="#3001-VOTE-073">3001-VOTE-073</a>)
For update market proposals:
- As a liquidity provider, **must** be able to participate in a liquidity vote on an update market proposal, when the token vote has not reached required participation level in time (<a name="3001-VOTE-015" href="#3001-VOTE-015">3001-VOTE-015</a>)
- **must** be able to to understand why a liquidity vote has been incurred (<a name="3001-VOTE-016" href="#3001-VOTE-016">3001-VOTE-016</a>)
- **must** be able to to understand that the result in the end may differ (as the outcome of the vote is based on the number of tokens held by the voters at time of vote close) (<a name="3001-VOTE-017" href="#3001-VOTE-017">3001-VOTE-017</a>)
For `closed` market change proposals, all of the above and:
- **must** see all of above but values at time of vote close (<a name="3001-VOTE-074" href="#3001-VOTE-074">3001-VOTE-074</a>)
... so I can see what I am voting for and the status of the vote.
## Can vote on an open proposals
When looking to vote on the proposal, I...
- **must** see an option to [connect to a Vega wallet/key](./0002-WCON-connect_vega_wallet.md) (<a name="3001-VOTE-075" href="#3001-VOTE-075">3001-VOTE-075</a>)
- **must** be [connected to a Vega wallet/key](./0002-WCON-connect_vega_wallet.md) (<a name="3001-VOTE-076" href="#3001-VOTE-076">3001-VOTE-076</a>)
- **must** see sum of tokens I have [associated](1027-ASSO-associate.md) (<a name="3001-VOTE-100" href="#3001-VOTE-100">3001-VOTE-100</a>)
- **should** see what percentage of the total [associated](1027-ASSO-associate.md) tokens I hold
- **must**, if I have 0 tokens, see link to [associate](1027-ASSO-associate.md) (<a name="3001-VOTE-012" href="#3001-VOTE-012">3001-VOTE-012</a>)
- **must** see my current vote for, against, or not voted (<a name="3001-VOTE-079" href="#3001-VOTE-079">3001-VOTE-079</a>)
- **must** see option to vote for or against (<a name="3001-VOTE-080" href="#3001-VOTE-080">3001-VOTE-080</a>)
- **must** trigger a transaction that needs to be confirmed in users wallet (<a name="3001-VOTE-013" href="#3001-VOTE-013">3001-VOTE-013</a>)
- **must** see that I need to confirm the transaction in my wallet to continue (<a name="3001-VOTE-014" href="#3001-VOTE-014">3001-VOTE-014</a>)
- **must** see option to change my vote (vote again in same or different direction) (<a name="3001-VOTE-090" href="#3001-VOTE-090">3001-VOTE-090</a>)
For open market change proposals, all of the above and:
- **must** see your equity like share on the market you are voting on (<a name="3001-VOTE-092" href="#3001-VOTE-092">3001-VOTE-092</a>)
for both:
- **must** see feedback of my vote [Vega transaction](0003-WTXN-submit_vega_transaction.md) (<a name="3001-VOTE-093" href="#3001-VOTE-093">3001-VOTE-093</a>)
...so that I can cast my vote and see the impact it might have.
+63
View File
@@ -0,0 +1,63 @@
# Propose
Background: [Governance spec](../protocol/0028-GOVE-governance.md)
and [docs](https://docs.vega.xyz/docs/mainnet/concepts/vega-protocol#governance).
## Associate tokens
When looking to propose something, I...
- **must** see see a link to detailed explanation of Governance in the docs (<a name="3002-PROP-001" href="#3002-PROP-001">3002-PROP-001</a>)
- **must** see that the governance token is an ethereum ERC20 token and needs to attributed (or [associated](1004-ASSO-associate.md)) to a Vega wallet for use on Vega (<a name="3002-PROP-002" href="#3002-PROP-002">3002-PROP-002</a>)
- **must** see that I need some associated tokens, and a link to [associate](1004-ASSO-associate.md) (<a name="3002-PROP-003" href="#3002-PROP-003">3002-PROP-003</a>)
... so I have a sufficient vote weight to propose.
## Select proposal type
When making a proposal, I...
- **must** select a proposal type (<a name="3002-PROP-005" href="#3002-PROP-005">3002-PROP-005</a>)
...so I get the appropriate form and information about rules for that type of proposal. e.g. min enactment and vote duration.
## Populate a proposal form
When making a proposal, I...
- **must** be able to see what they are to be used for on this site i.e. "Create proposal and download JSON to share" (<a name="3002-PROP-023" href="#3002-PROP-023">3002-PROP-023</a>)
- **must** input a rationale title and description (<a name="3002-PROP-006" href="#3002-PROP-006">3002-PROP-006</a>)
- **must** see the rules (min vote duration and enactment delay) for this proposal type (<a name="3002-PROP-008" href="#3002-PROP-008">3002-PROP-008</a>)
- **should** see the minimum required tokens or LP vote required to make a proposal Spam protection and network parameters
- if anything except market change: **must** be warned if the amount I have associated is less the the minimum required to propose for this proposal type (due to Spam protection or network parm, whichever is greater) (<a name="3002-PROP-009" href="#3002-PROP-009">3002-PROP-009</a>)
- if market change: **must** be warned if the amount of tokens I have less than the minimum tokens required to propose a change (due to Spam protection or network params) (<a name="3002-PROP-020" href="#3002-PROP-020">3002-PROP-020</a>)
- **must** see whether I have already voted or not on the card / list view (s) (<a name="3002-PROP-021" href="#3002-PROP-021">3002-PROP-021</a>)
- **should** see the balance of associated Governance tokens
### Detail on specific proposals
Go to the following for detail on each proposal type:
- [Propose new Market](./1006-PMARK-propose_new_market.md)
- [Propose change(s) to market](./1007-PMAC-propose_market_change.md)
- [Propose new asset](1008-PASN-propose_new_asset.md)
- [Propose change(s) to asset](1009-PASC-propose_asset_change.md)
- [Propose change to network parameter(s)](1010-PNEC-propose_network.md)
- [Propose something "Freeform"](1011-PFRO-propose_freeform.md)
### Submit proposal
- **must** submit the proposal [Vega transaction](0003-WTXN-submit_vega_transaction.md) (<a name="3002-PROP-011" href="#3002-PROP-011">3002-PROP-011</a>)
- **must** see the feedback on the [Vega transaction](0003-WTXN-submit_vega_transaction.md) (<a name="3002-PROP-012" href="#3002-PROP-012">3002-PROP-012</a>)
- If there is an error on the proposal:
- **must** be shown an error message will all of the error details from the API (<a name="3002-PROP-013" href="#3002-PROP-013">3002-PROP-013</a>)
- **must** see the proposal form populated with all the same values just submitted (<a name="3002-PROP-014" href="#3002-PROP-014">3002-PROP-014</a>)
- **should** see error messages highlighted on the inputs that require user attention
- If the proposal was successful:
- **must** be shown it was successful (<a name="3002-PROP-016" href="#3002-PROP-016">3002-PROP-016</a>)
- **should** be prompted to vote on the proposal
- **should** be prompted to share the proposal detail page to encourage others to vote
- **must** see a link to the proposal detail page (<a name="3002-PROP-019" href="#3002-PROP-019">3002-PROP-019</a>)
...so that the proposal is listed on the chain and I and others can vote for or against it.
+35
View File
@@ -0,0 +1,35 @@
# Propose new Market
**must** be a way to create a new proposal
(<a name="3003-PMAN-001" href="#3003-PMAN-001">3003-PMAN-001</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3003-PMAN-002" href="#3003-PMAN-002">3003-PMAN-002 </a>)
- **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3003-PMAN-003" href="#3003-PMAN-003">3003-PMAN-003</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3003-PMAN-004" href="#3003-PMAN-004">3003-PMAN-004 </a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3003-PMAN-005" href="#3003-PMAN-005">3003-PMAN-005 </a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3003-PMAN-006" href="#3003-PMAN-006">3003-PMAN-006</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3003-PMAN-007" href="#3003-PMAN-007">3003-PMAN-007</a>)
# Successor Market
- **must** On the home page and proposal listing page summary, I can see which market proposals are successor markets (<a name="3003-PMAN-008" href="#3003-PMAN-008">3003-PMAN-008</a>)
- **must** On a proposal details view that is a successor market, I can see the Parent market ID and Insurance pool fraction in the market specification (<a name="3003-PMAN-009" href="#3003-PMAN-009">3003-PMAN-009</a>)
- **must** On a proposal details view that is a successor market, I can see the Parent market ID and Insurance pool fraction in the proposal JSON (<a name="3003-PMAN-010" href="#3003-PMAN-010">3003-PMAN-009</a>)
- **must** I can see an explanation of what "Parent market ID" using a tooltip (<a name="3003-PMAN-011" href="#3003-PMAN-011">3003-PMAN-011</a>)
If the proposal has been enacted, I can see the status of the market i.e. opening auction, continuous trading, rejected
back to [Propose](./3002-PROP-propose.md) for details on proposing
+24
View File
@@ -0,0 +1,24 @@
## Propose market change
- **must** be a way to create a new proposal
(<a name="3004-PMAC-001" href="#3004-PMAC-001">3004-PMAC-001</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3004-PMAC-002" href="#3004-PMAC-002">3004-PMAC-002 </a>)
- **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3004-PMAC-003" href="#3004-PMAC-003">3004-PMAC-003</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3004-PMAC-004" href="#3004-PMAC-004">3004-PMAC-004 </a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3004-PMAC-005" href="#3004-PMAC-005">3004-PMAC-005 </a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3004-PMAC-006" href="#3004-PMAC-006">3004-PMAC-006</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3004-PMAC-007" href="#3004-PMAC-007">3004-PMAC-007</a>)
back to [Propose](./3002-PROP-propose.md) for details on proposing
+24
View File
@@ -0,0 +1,24 @@
# Propose new Asset
- **must** be a way to create a new proposal
(<a name="3005-PASN-001" href="#3005-PASN-001">3005-PASN-001</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3005-PASN-002" href="#3005-PASN-002">3005-PASN-002 </a>)
- **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3005-PASN-003" href="#3005-PASN-003">3005-PASN-003</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3005-PASN-004" href="#3005-PASN-004">3005-PASN-004 </a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3005-PASN-005" href="#3005-PASN-005">3005-PASN-005 </a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3005-PASN-006" href="#3005-PASN-006">3005-PASN-006</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3005-PASN-007" href="#3005-PASN-007">3005-PASN-007</a>)
back to [Propose](./3002-PROP-propose.md) for details on proposing
+26
View File
@@ -0,0 +1,26 @@
## Propose asset change
- **must** be a way to create a new proposal
(<a name="3006-PASC-001" href="#3006-PASC-001">3006-PASC-001</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3006-PASC-002" href="#3006-PASC-002">3006-PASC-002 </a>)
- **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3006-PASC-003" href="#3006-PASC-003">3006-PASC-003</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3006-PASC-004" href="#3006-PASC-004">3006-PASC-004 </a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3006-PASC-005" href="#3006-PASC-005">3006-PASC-005 </a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3006-PASC-006" href="#3006-PASC-006">3006-PASC-006</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3006-PASC-007" href="#3006-PASC-007">3006-PASC-007</a>)
## Submit proposal
back to [Propose](./3002-PROP-propose.md) for details on proposing
+72
View File
@@ -0,0 +1,72 @@
# Propose change to network parameter(s)
- **must** be a way to create a new proposal
(<a name="3007-PNEC-001" href="#3007-PNEC-001">3007-PNEC-001</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3007-PNEC-002" href="#3007-PNEC-002">3007-PNEC-002 </a>)
- **should** be directed from the docs to use the Governance dApp to help put together a Network Parameter proposal
(<a name="3007-PNEC-003" href="#3007-PNEC-003">3007-PNEC-003</a>)
- **must** be able to give my proposal a title
(<a name="3007-PNEC-004" href="#3007-PNEC-004">3007-PNEC-004</a>)
- **must** be able to see what the title would be used for
(<a name="3007-PNEC-005" href="#3007-PNEC-005">3007-PNEC-005</a>)
- **must** be constrained to a character limit and can understand what that limit is
(<a name="3007-PNEC-006" href="#3007-PNEC-006">3007-PNEC-006</a>)
- **must** be a way to add a proposal description
(<a name="3007-PNEC-007" href="#3007-PNEC-007">3007-PNEC-007</a>)
- **must** see what the description would be used for
(<a name="3007-PNEC-008" href="#3007-PNEC-008">3007-PNEC-008</a>)
- **must** be constrained to a character limit and can understand what that limit is
(<a name="3007-PNEC-009" href="#3007-PNEC-009">3007-PNEC-009</a>)
- **must** be able to select the parameter I want to change
(<a name="3007-PNEC-010" href="#3007-PNEC-010">3007-PNEC-010</a>)
- **must** see the current value and enter a proposed value (the change I want)
(<a name="3007-PNEC-011" href="#3007-PNEC-011">3007-PNEC-011</a>)
- **must** be a way to set when voting will close and a way to set when the proposal should be enacted
(<a name="3007-PNEC-012" href="#3007-PNEC-012">3007-PNEC-012</a>)
- **must** a way to set voting close to the maximum or minimum amount of time
(<a name="3007-PNEC-013" href="#3007-PNEC-013">3007-PNEC-013</a>)
- **must** a way to understand when that will be (date, time)
(<a name="3007-PNEC-014" href="#3007-PNEC-014">3007-PNEC-014</a>)
- **must** a way to set proposal enactment to the maximum or minimum amount of time
(<a name="3007-PNEC-015" href="#3007-PNEC-015">3007-PNEC-015</a>)
- **must** a way to understand when that will be (date, time)
(<a name="3007-PNEC-016" href="#3007-PNEC-016">3007-PNEC-016</a>)
- **must** a way to understand how the deadline and enactment times must relate i.e. must be equal to or after vote close
(<a name="3007-PNEC-017" href="#3007-PNEC-017">3007-PNEC-017</a>)
- **must** be able to download the proposal as JSON ready to share with the community on the forum
(<a name="3007-PNE-018" href="#3007-PNE-018">3007-PNE-018</a>)
- When I have received feedback from the community, I **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3007-PNE-019" href="#3007-PNE-019">3007-PNE-019</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3007-PNE-020" href="#3007-PNE-020">3007-PNE-020</a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3007-PNE-021" href="#3007-PNE-021">3007-PNE-021</a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3007-PNE-022" href="#3007-PNE-022">3007-PNE-022</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3007-PNE-023" href="#3007-PNE-023">3007-PNE-023</a>)
back to [Propose](./3002-PROP-propose.md) for details on proposing
+59
View File
@@ -0,0 +1,59 @@
# Propose freeform
- **must** not see anything enactment field(because freeform do not enact) (<a name="3008-PFRO-001" href="#3008-PFRO-001">3008-PFRO-001</a>)
- **must** be a way to create a new proposal
(<a name="3008-PFRO-002" href="#3008-PFRO-002">3008-PFRO-002</a>)
- **must** be directed to the docs in order to create my proposal where I can see the instructions and steps I need to go through
(<a name="3008-PFRO-003" href="#3008-PFRO-003">3008-PFRO-003</a>)
- **should** be directed from the docs to use the Governance dApp to help put together a freeform proposal
(<a name="3008-PFRO-004" href="#3008-PFRO-004">3008-PFRO-004</a>)
- **must** be able to give my proposal a title
(<a name="3008-PFRO-005" href="#3008-PFRO-005">3008-PFRO-005</a>)
- **must** be able to see what the title would be used for
(<a name="3008-PFRO-006" href="#3008-PFRO-006">3008-PFRO-006</a>)
- **must** be constrained to a character limit and can understand what that limit is
(<a name="3008-PFRO-007" href="#3008-PFRO-007">3008-PFRO-007</a>)
- **must** be a way to add a proposal description
(<a name="3008-PFRO-008" href="#3008-PFRO-008">3008-PFRO-008</a>)
- **must** see what the description would be used for
(<a name="3008-PFRO-009" href="#3008-PFRO-009">3008-PFRO-009</a>)
- **must** be constrained to a character limit and can understand what that limit is
(<a name="3008-PFRO-010" href="#3008-PFRO-010">3008-PFRO-010</a>)
- **must** be a way to set when voting will close
(<a name="3008-PFRO-011" href="#3008-PFRO-011">3008-PFRO-011</a>)
- **must** a way to set voting close to the maximum or minimum amount of time
(<a name="3008-PFRO-012" href="#3008-PFRO-012">3008-PFRO-012</a>)
- **must** a way to understand when that will be (date, time)
(<a name="3008-PFRO-013" href="#3008-PFRO-013">3008-PFRO-013</a>)
- **must** be able to download the proposal as JSON ready to share with the community on the forum
(<a name="3008-PFRO-014" href="#3008-PFRO-014">3008-PFRO-014</a>)
- When I have received feedback from the community, I **should** be directed to submit my proposal on the governance site where I can paste in my raw JSON
(<a name="3008-PFRO-015" href="#3008-PFRO-015">3008-PFRO-015</a>)
- **must** be able to see how many VEGA I must have associated with a validator in order to submit a proposal
(<a name="3008-PFRO-016" href="#3008-PFRO-016">3008-PFRO-016</a>)
- **must** be a way to find more information on submitting a proposal (a link to docs) and explorer (a link to explorer / governance)
(<a name="3008-PFRO-017" href="#3008-PFRO-017">3008-PFRO-017</a>)
- **must** error when submitting a proposal without entering anything, telling me JSON is required
(<a name="3008-PFRO-018" href="#3008-PFRO-018">3008-PFRO-018</a>)
- **must** error when submitting a proposal in anything other than JSON, telling me the proposal must be valid JSON
(<a name="3008-PFRO-019" href="#3008-PFRO-019">3008-PFRO-019</a>)
back to [Propose](./3002-PROP-propose.md) for details on proposing
+21
View File
@@ -0,0 +1,21 @@
# Network upgrade proposal
- **must** be able to see the network upgrade announcement from anywhere on the governance dapp (<a name="3009-NTWU-001" href="#3009-NTWU-001">3009-NTWU-001</a>)
- **must** be able to see 'what' version of the protocol the network is planned to be upgraded to (<a name="3009-NTWU-002" href="#3009-NTWU-002">3009-NTWU-002</a>)
- **must** be able to see what block height the upgrade will run at (<a name="3009-NTWU-003" href="#3009-NTWU-003">3009-NTWU-003</a>)
- **must** be able to see the current block height (<a name="3009-NTWU-004" href="#3009-NTWU-004">3009-NTWU-004</a>)
- **must** understand the impact(s) of the upgrade i.e. how long the network might be offline etc. (<a name="3009-NTWU-005" href="#3009-NTWU-005">3009-NTWU-005</a>)
- **must** be able to see an up to date estimate of / countdown to 'when' the planned upgrade is estimated to take place (<a name="3009-NTWU-006" href="#3009-NTWU-006">3009-NTWU-006</a>)
- **must** be able to see all the details of the upgrade proposal (dependent on this ticket is done Basic network upgrade proposal details page) (<a name="3009-NTWU-007" href="#3009-NTWU-007">3009-NTWU-007</a>)
- **must** be able to see in detail view of a network upgrade proposal, the estimated time to upgrade i.e. Estimated time to upgrade: 1h56m (13:00 UTC) (<a name="3009-NTWU-008" href="#3009-NTWU-008">3009-NTWU-008</a>)
- **must** be able to see when a network upgrade proposal block number has been passed.
i.e. the time to upgrade is replaced with Estimated upgrade complete: 24 May 2023 13:00UTC
(<a name="3009-NTWU-009" href="#3009-NTWU-009">3009-NTWU-009</a>)
+1
View File
@@ -0,0 +1 @@
# View treasury rewards
@@ -0,0 +1 @@
# Find liquidity provision opportunities
+37
View File
@@ -0,0 +1,37 @@
# Liquidity
## Trading and liquidity view
- **Must** be able to view liquidity table (<a name="5002-LIQP-001" href="#5002-LIQP-001">5002-LIQP-001</a>)
- **Must** be able to see the following columns (<a name="5002-LIQP-002" href="#5002-LIQP-002">5002-LIQP-002</a>):
- party
- commitment
- share
- proposed fee
- market valuation at entry
- obligation
- supplied
- status
- created
- updated
- **Must** be able to sort each column by asc and dsc (<a name="5002-LIQP-003" href="#5002-LIQP-003">5002-LIQP-003</a>)
## Liquidity view
- **Must** be able to see the following sections
- name (<a name="5002-LIQP-004" href="#5002-LIQP-004">5002-LIQP-004</a>)
- trading page link (<a name="5002-LIQP-005" href="#5002-LIQP-005">5002-LIQP-005</a>)
- target stake (<a name="5002-LIQP-006" href="#5002-LIQP-006">5002-LIQP-006</a>)
- supplied stake (<a name="5002-LIQP-007" href="#5002-LIQP-007">5002-LIQP-007</a>)
- liquidity supplied (<a name="5002-LIQP-008" href="#5002-LIQP-008">5002-LIQP-008</a>)
- market id (<a name="5002-LIQP-009" href="#5002-LIQP-009">5002-LIQP-009</a>)
- learn more (<a name="5002-LIQP-010" href="#5002-LIQP-010">5002-LIQP-010</a>)
- **Must** be able to see the following tabs
- active (<a name="5002-LIQP-011" href="#5002-LIQP-011">5002-LIQP-011</a>)
- inactive (<a name="5002-LIQP-012" href="#5002-LIQP-012">5002-LIQP-012</a>)
- **Must** be able to see the number values formatted using quantum and asset decimals, on hover full value should be displayed (<a name="5002-LIQP-013" href="#5002-LIQP-013">5002-LIQP-013</a>)
+1
View File
@@ -0,0 +1 @@
# Review liquidity income

Some files were not shown because too many files have changed in this diff Show More