Compare commits

..
57 changed files with 811 additions and 1011 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
* @vegaprotocol/frontend @vegaprotocol/frontend-qa * @vegaprotocol/frontend
* @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core *.graphql @vegaprotocol/core
+1 -31
View File
@@ -718,8 +718,6 @@
"UpdateAssetProposal": "Update asset proposal", "UpdateAssetProposal": "Update asset proposal",
"NewFreeformProposal": "New freeform proposal", "NewFreeformProposal": "New freeform proposal",
"NewRawProposal": "New proposal", "NewRawProposal": "New proposal",
"NewTransferProposal": "New transfer proposal",
"CancelTransferProposal": "Cancel transfer proposal",
"MARKET_STATE_UPDATE_TYPE_RESUME": "Resume market", "MARKET_STATE_UPDATE_TYPE_RESUME": "Resume market",
"MARKET_STATE_UPDATE_TYPE_SUSPEND": "Suspend market", "MARKET_STATE_UPDATE_TYPE_SUSPEND": "Suspend market",
"MARKET_STATE_UPDATE_TYPE_TERMINATE": "Terminate market", "MARKET_STATE_UPDATE_TYPE_TERMINATE": "Terminate market",
@@ -740,8 +738,6 @@
"UpdateVolumeDiscountProgram": "Update volume discount program", "UpdateVolumeDiscountProgram": "Update volume discount program",
"NewAsset": "New asset", "NewAsset": "New asset",
"UpdateAsset": "Update asset", "UpdateAsset": "Update asset",
"NewTransfer": "New transfer",
"CancelTransfer": "Cancel transfer",
"AssetID": "Asset ID", "AssetID": "Asset ID",
"Freeform": "Freeform", "Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)", "RawProposal": "Let me choose (raw proposal)",
@@ -918,31 +914,5 @@
"EndOfProgramTimestamp": "End of program", "EndOfProgramTimestamp": "End of program",
"EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.", "EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.",
"BenefitTierVolumeDiscountFactor": "Volume discount factor", "BenefitTierVolumeDiscountFactor": "Volume discount factor",
"BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier", "BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier"
"ACCOUNT_TYPE_INSURANCE": "Insurance account",
"ACCOUNT_TYPE_GLOBAL_INSURANCE": "Global insurance account",
"ACCOUNT_TYPE_SETTLEMENT": "Settlement account",
"ACCOUNT_TYPE_MARGIN": "Margin account ",
"ACCOUNT_TYPE_GENERAL": "General account",
"ACCOUNT_TYPE_FEES_INFRASTRUCTURE": "Infrastructure fees account",
"ACCOUNT_TYPE_FEES_LIQUIDITY": "Liquidity fees account",
"ACCOUNT_TYPE_FEES_MAKER": "Maker fees account",
"ACCOUNT_TYPE_BOND": "Bond account",
"ACCOUNT_TYPE_EXTERNAL": "External account",
"ACCOUNT_TYPE_GLOBAL_REWARD": "Global reward account",
"ACCOUNT_TYPE_PENDING_TRANSFERS": "Pending transfers account",
"ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES": "Maker paid fees reward account",
"ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES": "Maker received fees reward account",
"ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES": "Liquidity provider received fees reward account",
"ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS": "Market proposers reward account",
"ACCOUNT_TYPE_HOLDING": "Holding account",
"ACCOUNT_TYPE_LP_LIQUIDITY_FEES": "Liquidity provider fees account",
"ACCOUNT_TYPE_NETWORK_TREASURY": "Network treasury account",
"ACCOUNT_TYPE_VESTING_REWARDS": "Vesting rewards account",
"ACCOUNT_TYPE_VESTED_REWARDS": "Vested rewards account",
"ACCOUNT_TYPE_REWARD_AVERAGE_POSITION": "Average position reward account",
"ACCOUNT_TYPE_REWARD_RELATIVE_RETURN": "Relative return reward account",
"ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY": "Return volatility reward account",
"ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING": "Validator ranking reward account",
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account"
} }
@@ -263,10 +263,7 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
return ( return (
<span> <span>
{GovernanceTransferKindMapping[details.kind.__typename]}{' '} {GovernanceTransferKindMapping[details.kind.__typename]}{' '}
{t('transfer from')}{' '} {t('transfer from')} <Lozenge>{truncateMiddle(details.source)}</Lozenge>{' '}
<Lozenge>
{truncateMiddle(details.source) || t(details.sourceType)}
</Lozenge>{' '}
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge> {t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
</span> </span>
); );
@@ -63,42 +63,44 @@ const mockReferralProposal = generateProposal({
terms: { terms: {
change: { change: {
__typename: 'UpdateReferralProgram', __typename: 'UpdateReferralProgram',
benefitTiers: [ changes: {
{ benefitTiers: [
minimumEpochs: 6, {
minimumRunningNotionalTakerVolume: '10000', minimumEpochs: 6,
referralDiscountFactor: '0.001', minimumRunningNotionalTakerVolume: '10000',
referralRewardFactor: '0.001', referralDiscountFactor: '0.001',
}, referralRewardFactor: '0.001',
{ },
minimumEpochs: 24, {
minimumRunningNotionalTakerVolume: '500000', minimumEpochs: 24,
referralDiscountFactor: '0.005', minimumRunningNotionalTakerVolume: '500000',
referralRewardFactor: '0.005', referralDiscountFactor: '0.005',
}, referralRewardFactor: '0.005',
{ },
minimumEpochs: 48, {
minimumRunningNotionalTakerVolume: '1000000', minimumEpochs: 48,
referralDiscountFactor: '0.01', minimumRunningNotionalTakerVolume: '1000000',
referralRewardFactor: '0.01', referralDiscountFactor: '0.01',
}, referralRewardFactor: '0.01',
], },
endOfProgram: '2026-10-03T10:34:34Z', ],
windowLength: 3, endOfProgramTimestamp: '2026-10-03T10:34:34Z',
stakingTiers: [ windowLength: 3,
{ stakingTiers: [
minimumStakedTokens: '1', {
referralRewardMultiplier: '1', minimumStakedTokens: '1',
}, referralRewardMultiplier: '1',
{ },
minimumStakedTokens: '2', {
referralRewardMultiplier: '2', minimumStakedTokens: '2',
}, referralRewardMultiplier: '2',
{ },
minimumStakedTokens: '5', {
referralRewardMultiplier: '3', minimumStakedTokens: '5',
}, referralRewardMultiplier: '3',
], },
],
},
}, },
}, },
}); });
@@ -130,6 +132,7 @@ describe('<ProposalReferralProgramDetails />', () => {
terms: { terms: {
change: { change: {
__typename: 'UpdateReferralProgram', __typename: 'UpdateReferralProgram',
changes: {},
}, },
}, },
}); });
@@ -54,10 +54,11 @@ export const ProposalReferralProgramDetails = ({
return null; return null;
} }
const benefitTiers = proposal?.terms?.change?.benefitTiers; const benefitTiers = proposal?.terms?.change?.changes?.benefitTiers;
const stakingTiers = proposal?.terms?.change?.stakingTiers; const stakingTiers = proposal?.terms?.change?.changes?.stakingTiers;
const windowLength = proposal?.terms?.change?.windowLength; const windowLength = proposal?.terms?.change?.changes?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram; const endOfProgramTimestamp =
proposal?.terms?.change?.changes?.endOfProgramTimestamp;
if ( if (
!benefitTiers && !benefitTiers &&
@@ -30,8 +30,6 @@ export const useProposalNetworkParams = ({
NetworkParams.governance_proposal_freeform_requiredParticipation, NetworkParams.governance_proposal_freeform_requiredParticipation,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority, NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation, NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredMajority,
]); ]);
const fallback = { const fallback = {
@@ -113,14 +111,6 @@ export const useProposalNetworkParams = ({
params.governance_proposal_VolumeDiscountProgram_requiredParticipation params.governance_proposal_VolumeDiscountProgram_requiredParticipation
), ),
}; };
case 'NewTransfer':
case 'CancelTransfer':
return {
requiredMajority: params.governance_proposal_transfer_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation
),
};
default: default:
return fallback; return fallback;
} }
@@ -47,17 +47,19 @@ fragment UpdateReferralProgram on Proposal {
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
benefitTiers { changes {
minimumEpochs benefitTiers {
minimumRunningNotionalTakerVolume minimumEpochs
referralDiscountFactor minimumRunningNotionalTakerVolume
referralRewardFactor referralDiscountFactor
} referralRewardFactor
endOfProgram: endOfProgramTimestamp }
windowLength endOfProgramTimestamp
stakingTiers { windowLength
minimumStakedTokens stakingTiers {
referralRewardMultiplier minimumStakedTokens
referralRewardMultiplier
}
} }
} }
} }
File diff suppressed because one or more lines are too long
@@ -47,17 +47,19 @@ fragment UpdateReferralPrograms on Proposal {
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
benefitTiers { changes {
minimumEpochs benefitTiers {
minimumRunningNotionalTakerVolume minimumEpochs
referralDiscountFactor minimumRunningNotionalTakerVolume
referralRewardFactor referralDiscountFactor
} referralRewardFactor
endOfProgram: endOfProgramTimestamp }
windowLength endOfProgramTimestamp
stakingTiers { windowLength
minimumStakedTokens stakingTiers {
referralRewardMultiplier minimumStakedTokens
referralRewardMultiplier
}
} }
} }
} }
@@ -7,7 +7,7 @@ export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: {
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } }; export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } }; export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } }; export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
@@ -21,7 +21,7 @@ export type ProposalsQueryVariables = Types.Exact<{
}>; }>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null }; export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const NewMarketProductFieldsFragmentDoc = gql` export const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal { fragment NewMarketProductFields on Proposal {
@@ -75,17 +75,19 @@ export const UpdateReferralProgramsFragmentDoc = gql`
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
benefitTiers { changes {
minimumEpochs benefitTiers {
minimumRunningNotionalTakerVolume minimumEpochs
referralDiscountFactor minimumRunningNotionalTakerVolume
referralRewardFactor referralDiscountFactor
} referralRewardFactor
endOfProgram: endOfProgramTimestamp }
windowLength endOfProgramTimestamp
stakingTiers { windowLength
minimumStakedTokens stakingTiers {
referralRewardMultiplier minimumStakedTokens
referralRewardMultiplier
}
} }
} }
} }
@@ -0,0 +1,201 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const assetSelectField = 'select[name="asset"]';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
const formFieldError = 'input-error-text';
const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => {
function openDepositForm() {
cy.mockWeb3Provider();
cy.mockSubscription();
cy.mockTradingPage();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.wait('@Assets');
}
before(() => {
openDepositForm();
});
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
// once Ethereum wallet is connected and key selected the only field that will
// error is the asset select
cy.getByTestId(formFieldError).should('have.length', 1);
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
'have.length',
1
);
});
it('unable to select assets not enabled', () => {
// Assets not enabled in mocks
cy.get(assetSelectField + ' option:contains(Asset 2)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 3)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
});
it('invalid public key when entering address manually', () => {
cy.getByTestId('enter-pubkey-manually').click();
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
'have.text',
'Invalid Vega key'
);
});
it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO);
cy.get(amountField)
.clear()
.type('0.00000000000000000000000000000000001')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Value is below minimum');
});
it('insufficient funds', () => {
// 1001-DEPO-004
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
cy.get(amountField)
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
});
});
describe('deposit actions', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/markets/market-1');
});
it.skip('Deposit to trade is visible', () => {
cy.getByTestId('Collateral').click();
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}
@@ -0,0 +1,91 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const transferForm = 'transfer-form';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -0,0 +1,5 @@
import MarketPage from '../market';
export const ClosedMarketPage = () => {
return <MarketPage closed />;
};
@@ -0,0 +1 @@
export { ClosedMarketPage as default } from './closed-market';
@@ -1,48 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LiquiditySidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
@@ -43,7 +43,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const asset = getAsset(market); const asset = getAsset(market);
return ( return (
<> <div className="flex gap-8">
<HeaderStat heading={t('Mark Price')} testId="market-price"> <HeaderStat heading={t('Mark Price')} testId="market-price">
<MarketMarkPrice <MarketMarkPrice
marketId={market.id} marketId={market.id}
@@ -144,7 +144,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
</HeaderStat> </HeaderStat>
)} )}
<MarketProposalNotification marketId={market.id} /> <MarketProposalNotification marketId={market.id} />
</> </div>
); );
}; };
+19 -4
View File
@@ -9,9 +9,10 @@ import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid'; import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels'; import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links'; import { Links, Routes } from '../../lib/links';
import { ViewType, useSidebar } from '../../components/sidebar'; import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { MarketState } from '@vegaprotocol/types';
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => { const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
return markPrice && decimalPlaces return markPrice && decimalPlaces
@@ -56,7 +57,7 @@ const TitleUpdater = ({
return null; return null;
}; };
export const MarketPage = () => { export const MarketPage = ({ closed }: { closed?: boolean }) => {
const { marketId } = useParams(); const { marketId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
@@ -69,11 +70,25 @@ export const MarketPage = () => {
const { data, loading } = useMarket(marketId); const { data, loading } = useMarket(marketId);
useEffect(() => {
if (
data?.state &&
[
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(data.state) &&
currentRouteId !== Routes.CLOSED_MARKETS &&
marketId
) {
navigate(Links.CLOSED_MARKETS(marketId));
}
}, [data?.state, currentRouteId, navigate, marketId]);
useEffect(() => { useEffect(() => {
if (data?.id && data.id !== lastMarketId && !closed) { if (data?.id && data.id !== lastMarketId && !closed) {
update({ marketId: data.id }); update({ marketId: data.id });
} }
}, [update, lastMarketId, data?.id]); }, [update, lastMarketId, data?.id, closed]);
useEffect(() => { useEffect(() => {
if (largeScreen && view === undefined) { if (largeScreen && view === undefined) {
@@ -82,7 +97,7 @@ export const MarketPage = () => {
currentRouteId currentRouteId
); );
} }
}, [setViews, view, currentRouteId, largeScreen]); }, [setViews, view, currentRouteId, largeScreen, closed]);
const pinnedAsset = data && getAsset(data); const pinnedAsset = data && getAsset(data);
@@ -73,19 +73,10 @@ const MainGrid = memo(
{market && {market &&
market.tradableInstrument.instrument.product.__typename === market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? ( 'Perpetual' ? (
<Tab id="funding-history" name={t('Funding history')}> <Tab id="funding" name={t('Funding')}>
<TradingViews.funding.component marketId={marketId} /> <TradingViews.funding.component marketId={marketId} />
</Tab> </Tab>
) : null} ) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component
marketId={marketId}
/>
</Tab>
) : null}
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
</ResizableGridPanel> </ResizableGridPanel>
@@ -148,6 +139,9 @@ const MainGrid = memo(
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<TradingViews.fills.component /> <TradingViews.fills.component />
</Tab> </Tab>
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component />
</Tab>
<Tab <Tab
id="accounts" id="accounts"
name={t('Collateral')} name={t('Collateral')}
@@ -363,12 +363,12 @@ describe('Closed', () => {
const container = within( const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement document.querySelector('.ag-center-cols-container') as HTMLElement
); );
const cell = container.getAllByRole('gridcell', {
name: (_name, element) => element.getAttribute('col-id') === 'code',
})[0];
const cells = await container.findAllByRole('gridcell'); expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
const cell = cells.find((el) => el.getAttribute('col-id') === 'code'); 'PRNT'
);
expect(
within(cell as HTMLElement).getByTestId('stack-cell-secondary')
).toHaveTextContent('PRNT');
}); });
}); });
+6 -9
View File
@@ -1,4 +1,3 @@
import type { CellClickedEvent } from 'ag-grid-community';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import type { import type {
@@ -6,7 +5,6 @@ import type {
VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { AgGrid, COL_DEFS } from '@vegaprotocol/datagrid'; import { AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { Asset } from '@vegaprotocol/types'; import type { Asset } from '@vegaprotocol/types';
@@ -19,11 +17,13 @@ import {
import { closedMarketsWithDataProvider, getAsset } from '@vegaprotocol/markets'; import { closedMarketsWithDataProvider, getAsset } from '@vegaprotocol/markets';
import type { DataSourceFilterFragment } from '@vegaprotocol/markets'; import type { DataSourceFilterFragment } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SettlementDateCell } from './settlement-date-cell'; import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell'; import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { MarketCodeCell } from './market-code-cell'; import { MarketCodeCell } from './market-code-cell';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
import type { CellClickedEvent } from 'ag-grid-community';
import { useClosedMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
type SettlementAsset = Pick< type SettlementAsset = Pick<
Asset, Asset,
@@ -127,7 +127,7 @@ const ClosedMarketsDataGrid = ({
rowData: Row[]; rowData: Row[];
error: Error | undefined; error: Error | undefined;
}) => { }) => {
const handleOnSelect = useMarketClickHandler(); const handleOnSelect = useClosedMarketClickHandler();
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open); const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
const colDefs = useMemo(() => { const colDefs = useMemo(() => {
@@ -302,11 +302,8 @@ const ClosedMarketsDataGrid = ({
return; return;
} }
handleOnSelect( // @ts-ignore metaKey exists
data.id, handleOnSelect(data.id, event ? event.metaKey : false);
// @ts-ignore metaKey exists
event ? event.metaKey : false
);
}} }}
/> />
); );
@@ -1,67 +0,0 @@
import { Route, Routes, useParams } from 'react-router-dom';
import { MarketState } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { useMarket } from '@vegaprotocol/markets';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const MarketsSidebar = () => {
const { marketId } = useParams();
const currentRouteId = useGetCurrentRouteId();
const { data } = useMarket(marketId);
const active =
data &&
[MarketState.STATE_ACTIVE, MarketState.STATE_PENDING].includes(data.state);
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<Routes>
<Route
path=":marketId"
element={
<>
<SidebarDivider />
{active && (
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
)}
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
}
/>
</Routes>
</>
);
};
@@ -1,31 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const PortfolioSidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</>
);
};
@@ -9,11 +9,7 @@ import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const FundingPaymentsContainer = ({ export const FundingPaymentsContainer = () => {
marketId,
}: {
marketId?: string;
}) => {
const onMarketClick = useMarketClickHandler(true); const onMarketClick = useMarketClickHandler(true);
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
@@ -37,7 +33,6 @@ export const FundingPaymentsContainer = ({
return ( return (
<FundingPaymentsManager <FundingPaymentsManager
partyId={pubKey} partyId={pubKey}
marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
gridProps={gridStoreCallbacks} gridProps={gridStoreCallbacks}
/> />
+3 -3
View File
@@ -17,11 +17,11 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
); );
return ( return (
<header className={headerClasses}> <header className={headerClasses}>
<div className="flex-col items-start justify-center hidden pt-2 pb-0 pl-3 lg:flex lg:pl-4 xl:pb-2"> <div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
{title} {title}
</div> </div>
<div data-testid="header-summary" className="min-w-0"> <div data-testid="header-summary" className="min-w-0">
<div className="flex items-center px-3 py-2 overflow-x-auto text-xs lg:px-4 flex-nowrap gap-6"> <div className="px-3 lg:px-4 py-2 flex flex-nowrap gap-4 items-center text-xs overflow-x-auto">
{children} {children}
</div> </div>
</div> </div>
@@ -70,7 +70,7 @@ export const HeaderTitle = ({ children }: { children: ReactNode }) => {
return ( return (
<h1 <h1
data-testid="header-title" data-testid="header-title"
className="flex items-center text-lg gap-4 whitespace-nowrap xl:pr-4 xl:border-r border-default" className="flex gap-4 items-center text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default"
> >
{children} {children}
</h1> </h1>
@@ -1,16 +1,11 @@
import type { ReactNode } from 'react'; import { Outlet, Routes, Route } from 'react-router-dom';
import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar'; import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames'; import classNames from 'classnames';
import { MarketHeader } from '../market-header';
import { LiquidityHeader } from '../liquidity-header';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = ({ export const LayoutWithSidebar = () => {
header,
sidebar,
}: {
header?: ReactNode;
sidebar?: ReactNode;
}) => {
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const views = useSidebar((store) => store.views); const views = useSidebar((store) => store.views);
const sidebarView = views[currentRouteId] || null; const sidebarView = views[currentRouteId] || null;
@@ -25,7 +20,16 @@ export const LayoutWithSidebar = ({
return ( return (
<div className={gridClasses}> <div className={gridClasses}>
<div className="col-span-full">{header}</div> <div className="col-span-full">
<Routes>
<Route path="markets/:marketId" element={<MarketHeader />} />
<Route
path="markets/all/closed/:marketId"
element={<MarketHeader />}
/>
<Route path="liquidity/:marketId" element={<LiquidityHeader />} />
</Routes>
</div>
<main <main
className={classNames('col-start-1 col-end-1', { className={classNames('col-start-1 col-end-1', {
'lg:col-end-3': !sidebarOpen, 'lg:col-end-3': !sidebarOpen,
@@ -36,12 +40,9 @@ export const LayoutWithSidebar = ({
</main> </main>
<aside <aside
// min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller // min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller
className={classNames( className={classNames('col-start-1 lg:col-start-2 min-h-0', {
'col-start-1 lg:col-start-2 min-h-0 lg:border-l border-default lg:bg-vega-clight-800 dark:lg:bg-vega-cdark-800', hidden: !sidebarOpen,
{ })}
hidden: !sidebarOpen,
}
)}
> >
<SidebarContent /> <SidebarContent />
</aside> </aside>
@@ -53,7 +54,7 @@ export const LayoutWithSidebar = ({
'lg:row-start-2 lg:row-span-full lg:col-start-3' 'lg:row-start-2 lg:row-span-full lg:col-start-3'
)} )}
> >
<Sidebar options={sidebar} /> <Sidebar />
</div> </div>
</div> </div>
); );
@@ -1,6 +1,6 @@
import { Popover, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { Popover, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Header, HeaderTitle } from '../header'; import { Header, HeaderTitle } from '../header';
import { Route, Routes, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { MarketSelector } from '../../components/market-selector/market-selector'; import { MarketSelector } from '../../components/market-selector/market-selector';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats'; import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { useMarket, useMarketList } from '@vegaprotocol/markets'; import { useMarket, useMarketList } from '@vegaprotocol/markets';
@@ -19,44 +19,36 @@ export const MarketHeader = () => {
if (!data) return null; if (!data) return null;
return ( return (
<Routes> <Header
<Route title={
path=":marketId" <Popover
element={ open={open}
<Header onChange={setOpen}
title={ trigger={
<Popover <HeaderTitle>
open={open} <span>
onChange={setOpen} {data.tradableInstrument.instrument.code}
trigger={ <span className="ml-2 text-xs uppercase text-muted">
<HeaderTitle> {' '}
<span> {data.tradableInstrument.instrument.product.__typename &&
{data.tradableInstrument.instrument.code} ProductTypeShortName[
<span className="ml-1 text-xs uppercase text-muted"> data.tradableInstrument.instrument.product.__typename
{data.tradableInstrument.instrument.product ]}{' '}
.__typename && </span>
ProductTypeShortName[ </span>
data.tradableInstrument.instrument.product <VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
.__typename </HeaderTitle>
]} }
</span> alignOffset={-10}
</span> >
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} /> <MarketSelector
</HeaderTitle> currentMarketId={marketId}
} onSelect={() => setOpen(false)}
alignOffset={-10} />
> </Popover>
<MarketSelector }
currentMarketId={marketId} >
onSelect={() => setOpen(false)} <MarketHeaderStats market={data} />
/> </Header>
</Popover>
}
>
<MarketHeaderStats market={data} />
</Header>
}
/>
</Routes>
); );
}; };
@@ -63,7 +63,7 @@ describe('Navbar', () => {
const expectedLinks = [ const expectedLinks = [
['/', ''], ['/', ''],
['/markets', 'Markets'], ['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'], [`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'], ['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'], ['/referrals', 'Referrals'],
@@ -80,7 +80,7 @@ describe('Navbar', () => {
}); });
it('Markets page route should not match empty market page', () => { it('Markets page route should not match empty market page', () => {
renderComponent(['/markets']); renderComponent(['/markets/all']);
expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active'); expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active');
expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass( expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass(
'active' 'active'
@@ -96,7 +96,7 @@ describe('Navbar', () => {
const menu = within(menuEl); const menu = within(menuEl);
const expectedLinks = [ const expectedLinks = [
['/markets', 'Markets'], ['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'], [`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'], ['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'], ['/referrals', 'Referrals'],
@@ -264,7 +264,6 @@ const NavbarLink = ({
<N.Link asChild={true}> <N.Link asChild={true}>
<NavLink <NavLink
to={to} to={to}
end={true}
className={classNames( className={classNames(
'block lg:flex lg:h-full flex-col justify-center', 'block lg:flex lg:h-full flex-col justify-center',
'px-6 py-2 lg:p-0 text-lg lg:text-sm', 'px-6 py-2 lg:p-0 text-lg lg:text-sm',
@@ -3,12 +3,14 @@ import { Filter, OrderListManager } from '@vegaprotocol/orders';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler'; import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { DataGridStore } from '../../stores/datagrid-store-slice'; import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
import { Links } from '../../lib/links';
export const FilterStatusValue = { export const FilterStatusValue = {
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED], [Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
@@ -30,7 +32,8 @@ const AUTO_SIZE_COLUMNS = ['instrument-code'];
export const OrdersContainer = ({ filter }: OrderContainerProps) => { export const OrdersContainer = ({ filter }: OrderContainerProps) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const navigate = useNavigateWithMeta(); const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const { gridState, updateGridState } = useOrderListGridState(filter); const { gridState, updateGridState } = useOrderListGridState(filter);
const gridStoreCallbacks = useDataGridEvents( const gridStoreCallbacks = useDataGridEvents(
gridState, gridState,
@@ -48,12 +51,8 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
<OrderListManager <OrderListManager
partyId={pubKey} partyId={pubKey}
filter={filter} filter={filter}
onMarketClick={(marketId, metaKey) => { onMarketClick={onMarketClick}
navigate(Links.MARKET(marketId), metaKey); onOrderTypeClick={onOrderTypeClick}
}}
onOrderTypeClick={(marketId, metaKey) =>
navigate(Links.LIQUIDITY(marketId), metaKey)
}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks} gridProps={gridStoreCallbacks}
/> />
@@ -38,68 +38,79 @@ const walletContext = {
} as VegaWalletContextShape; } as VegaWalletContextShape;
describe('Sidebar', () => { describe('Sidebar', () => {
beforeEach(() => { it.each(['/markets/all', '/portfolio'])(
useSidebar.setState({ views: {} }); 'does not render ticket and info',
}); (path) => {
render(
<VegaWalletContext.Provider value={walletContext}>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletContext.Provider>
);
it('renders options prop', () => { expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument();
// no order or info sidebars
expect(screen.queryByTestId(ViewType.Order)).not.toBeInTheDocument();
expect(screen.queryByTestId(ViewType.Info)).not.toBeInTheDocument();
}
);
it('renders ticket and info on market pages', () => {
render( render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
<MemoryRouter> <MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar options={<div data-testid="options" />} /> <Sidebar />
</MemoryRouter> </MemoryRouter>
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument(); expect(screen.getByTestId('node-health')).toBeInTheDocument();
expect(screen.getByTestId('options')).toBeInTheDocument();
// order and info sidebars are shown
expect(screen.getByTestId(ViewType.Order)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Info)).toBeInTheDocument();
}); });
it('renders selected state', async () => { it('renders selected state', async () => {
const routeId = '/markets/ABC';
render( render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
<MemoryRouter initialEntries={[routeId]}> <MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar <Sidebar />
options={
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip="Deposit"
routeId={'/markets/:marketId'}
/>
}
/>
</MemoryRouter> </MemoryRouter>
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
); );
const settingsButton = screen.getByTestId(ViewType.Settings); const settingsButton = screen.getByTestId(ViewType.Settings);
const depositButton = screen.getByTestId(ViewType.Deposit); const orderButton = screen.getByTestId(ViewType.Order);
// select settings first // select settings first
await userEvent.click(settingsButton); await userEvent.click(settingsButton);
expect(settingsButton).toHaveClass('bg-vega-yellow text-black'); expect(settingsButton).toHaveClass('bg-vega-yellow text-black');
// switch to deposit // switch to order
await userEvent.click(depositButton); await userEvent.click(orderButton);
expect(settingsButton).not.toHaveClass('bg-vega-yellow text-black'); expect(settingsButton).not.toHaveClass('bg-vega-yellow text-black');
expect(depositButton).toHaveClass('bg-vega-yellow text-black'); expect(orderButton).toHaveClass('bg-vega-yellow text-black');
// close order // close order
await userEvent.click(depositButton); await userEvent.click(orderButton);
expect(depositButton).not.toHaveClass('bg-vega-yellow text-black'); expect(orderButton).not.toHaveClass('bg-vega-yellow text-black');
}); });
}); });
describe('SidebarContent', () => { describe('SidebarContent', () => {
beforeEach(() => {
useSidebar.setState({ views: {} });
});
it('renders the correct content', () => { it('renders the correct content', () => {
const { container } = render( const { container } = render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
+98 -4
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { Route, Routes, useParams } from 'react-router-dom';
import { create } from 'zustand'; import { create } from 'zustand';
import { TransferContainer } from '@vegaprotocol/accounts'; import { TransferContainer } from '@vegaprotocol/accounts';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket'; import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
@@ -50,14 +50,108 @@ type SidebarView =
type: ViewType.Settings; type: ViewType.Settings;
}; };
export const Sidebar = ({ options }: { options?: ReactNode }) => { const MarketSidebarButtons = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
const AssetSidebarButtons = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</>
);
};
export const Sidebar = () => {
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
return ( return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar"> <div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>} <nav className={navClasses}>
<Routes>
<Route path="markets/all" element={<AssetSidebarButtons />} />
<Route path="portfolio">
<Route
// Show deposit/withdraw/transfer sidebar options only on portflio dashboard (index)
index={true}
element={<AssetSidebarButtons />}
/>
</Route>
<Route
path="markets/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<MarketSidebarButtons />
</>
}
/>
<Route
path="liquidity/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<MarketSidebarButtons />
</>
}
/>
<Route
path="markets/all/closed/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
}
/>
</Routes>
</nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton <SidebarButton
view={ViewType.ViewAs} view={ViewType.ViewAs}
@@ -140,7 +234,7 @@ export const SidebarButton = ({
); );
}; };
export const SidebarDivider = () => { const SidebarDivider = () => {
return ( return (
<div <div
className="w-px h-4 bg-vega-clight-600 dark:bg-vega-cdark-600 lg:w-4 lg:h-px" className="w-px h-4 bg-vega-clight-600 dark:bg-vega-cdark-600 lg:w-4 lg:h-px"
@@ -6,7 +6,7 @@ import { TelemetryApproval } from './telemetry-approval';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step'; import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step';
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_toast_id'; const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
export const Telemetry = () => { export const Telemetry = () => {
const onboardingDissmissed = useOnboardingStore((store) => store.dismissed); const onboardingDissmissed = useOnboardingStore((store) => store.dismissed);
@@ -1,4 +1,5 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
export const useMarketClickHandler = (replace = false) => { export const useMarketClickHandler = (replace = false) => {
@@ -14,10 +15,17 @@ export const useMarketClickHandler = (replace = false) => {
}; };
}; };
export const useNavigateWithMeta = (replace = false) => { export const useMarketLiquidityClickHandler = () => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
};
export const useClosedMarketClickHandler = (replace = false) => {
const navigate = useNavigate(); const navigate = useNavigate();
return (link: string, metaKey?: boolean) => { return (selectedId: string, metaKey?: boolean) => {
const link = Links.CLOSED_MARKETS(selectedId);
if (metaKey) { if (metaKey) {
window.open(`/#${link}`, '_blank'); window.open(`/#${link}`, '_blank');
} else { } else {
+4 -1
View File
@@ -4,7 +4,8 @@ import trimEnd from 'lodash/trimEnd';
// href creation // href creation
export const Routes = { export const Routes = {
HOME: '/', HOME: '/',
MARKETS: '/markets', MARKETS: '/markets/all',
CLOSED_MARKETS: '/markets/all/closed/:marketId',
MARKET: '/markets/:marketId', MARKET: '/markets/:marketId',
LIQUIDITY: '/liquidity/:marketId', LIQUIDITY: '/liquidity/:marketId',
PORTFOLIO: '/portfolio', PORTFOLIO: '/portfolio',
@@ -28,6 +29,8 @@ export const Links: ConsoleLinks = {
MARKET: (marketId: string) => MARKET: (marketId: string) =>
trimEnd(Routes.MARKET.replace(':marketId', marketId)), trimEnd(Routes.MARKET.replace(':marketId', marketId)),
MARKETS: () => Routes.MARKETS, MARKETS: () => Routes.MARKETS,
CLOSED_MARKETS: (marketId: string) =>
trimEnd(Routes.CLOSED_MARKETS.replace(':marketId', marketId)),
PORTFOLIO: () => Routes.PORTFOLIO, PORTFOLIO: () => Routes.PORTFOLIO,
LIQUIDITY: (marketId: string) => LIQUIDITY: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)), trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
+64 -70
View File
@@ -1,5 +1,5 @@
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom';
import { Navigate, useRoutes } from 'react-router-dom'; import { Navigate, Outlet, useRoutes } from 'react-router-dom';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
@@ -13,7 +13,7 @@ import { Assets } from '../client-pages/assets';
import { Deposit } from '../client-pages/deposit'; import { Deposit } from '../client-pages/deposit';
import { Withdraw } from '../client-pages/withdraw'; import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer'; import { Transfer } from '../client-pages/transfer';
import { Routes as AppRoutes } from '../lib/links'; import { Routes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout'; import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals'; import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics'; import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
@@ -22,15 +22,11 @@ import { CreateCodeContainer } from '../client-pages/referrals/create-code-form'
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary'; import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash'; import { compact } from 'lodash';
import { FLAGS } from '@vegaprotocol/environment'; import { FLAGS } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM // These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM // Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market')); const MarketPage = lazy(() => import('../client-pages/market'));
const ClosedMarketPage = lazy(() => import('../client-pages/closed-market'));
const Portfolio = lazy(() => import('../client-pages/portfolio')); const Portfolio = lazy(() => import('../client-pages/portfolio'));
const NotFound = () => ( const NotFound = () => (
@@ -40,21 +36,18 @@ const NotFound = () => (
); );
export const routerConfig: RouteObject[] = compact([ export const routerConfig: RouteObject[] = compact([
{ // Pages that don't use the LayoutWithSidebar must come first
index: true, // to ensure they are matched before the catch all route '/*'
element: <Home />,
id: AppRoutes.HOME,
},
{ {
path: 'disclaimer', path: 'disclaimer',
element: <LayoutCentered />, element: <LayoutCentered />,
id: AppRoutes.DISCLAIMER, id: Routes.DISCLAIMER,
children: [{ index: true, element: <Disclaimer /> }], children: [{ index: true, element: <Disclaimer /> }],
}, },
// Referrals routing (the pages should be available if the feature flag is on) // Referrals routing (the pages should be available if the feature flag is on)
FLAGS.REFERRALS FLAGS.REFERRALS
? { ? {
path: AppRoutes.REFERRALS, path: Routes.REFERRALS,
element: <LayoutWithSky />, element: <LayoutWithSky />,
children: [ children: [
{ {
@@ -65,11 +58,11 @@ export const routerConfig: RouteObject[] = compact([
element: <ReferralStatistics />, element: <ReferralStatistics />,
}, },
{ {
path: AppRoutes.REFERRALS_CREATE_CODE, path: Routes.REFERRALS_CREATE_CODE,
element: <CreateCodeContainer />, element: <CreateCodeContainer />,
}, },
{ {
path: AppRoutes.REFERRALS_APPLY_CODE, path: Routes.REFERRALS_APPLY_CODE,
element: <ApplyCodeForm />, element: <ApplyCodeForm />,
}, },
], ],
@@ -81,74 +74,75 @@ export const routerConfig: RouteObject[] = compact([
], ],
} }
: undefined, : undefined,
// All other pages will use the sidebar
{ {
path: 'markets/*', path: '/*',
element: ( element: <LayoutWithSidebar />,
<LayoutWithSidebar
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
),
children: [ children: [
{ {
index: true, index: true,
element: <MarketsPage />, element: <Home />,
id: AppRoutes.MARKETS, id: Routes.HOME,
}, },
{ {
path: 'all', path: 'markets',
element: <Navigate to="/markets" />, element: <Outlet />,
},
{
path: ':marketId',
element: <MarketPage />,
id: AppRoutes.MARKET,
},
],
},
{
path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Portfolio />,
id: AppRoutes.PORTFOLIO,
},
{
path: 'assets',
element: <Assets />,
id: AppRoutes.ASSETS,
children: [ children: [
{ index: true, element: <Navigate to="deposit" /> }, {
{ path: 'deposit', element: <Deposit />, id: AppRoutes.DEPOSIT }, index: true,
{ path: 'withdraw', element: <Withdraw />, id: AppRoutes.WITHDRAW }, element: <Navigate to="all" />,
{ path: 'transfer', element: <Transfer />, id: AppRoutes.TRANSFER }, },
{
path: 'all',
element: <MarketsPage />,
id: Routes.MARKETS,
},
{
path: ':marketId',
element: <MarketPage />,
id: Routes.MARKET,
},
{
path: 'all/closed/:marketId',
element: <ClosedMarketPage />,
id: Routes.CLOSED_MARKETS,
},
], ],
}, },
],
},
{
path: 'liquidity/*',
element: (
<LayoutWithSidebar
header={<LiquidityHeader />}
sidebar={<LiquiditySidebar />}
/>
),
id: AppRoutes.LIQUIDITY,
children: [
{ {
path: ':marketId', path: 'portfolio',
element: <Outlet />,
children: [
{
index: true,
element: <Portfolio />,
id: Routes.PORTFOLIO,
},
{
path: 'assets',
element: <Assets />,
id: Routes.ASSETS,
children: [
{ index: true, element: <Navigate to="deposit" /> },
{ path: 'deposit', element: <Deposit />, id: Routes.DEPOSIT },
{ path: 'withdraw', element: <Withdraw />, id: Routes.WITHDRAW },
{ path: 'transfer', element: <Transfer />, id: Routes.TRANSFER },
],
},
],
},
{
path: 'liquidity/:marketId',
element: <Liquidity />, element: <Liquidity />,
id: Routes.LIQUIDITY,
},
// NotFound page is here so its caught within parent '/*' route
{
path: '*',
element: <NotFound />,
}, },
], ],
}, },
{
path: '*',
element: <NotFound />,
},
]); ]);
export const ClientRouter = () => { export const ClientRouter = () => {
+6 -5
View File
@@ -1,8 +1,8 @@
import { pseudoRandom } from '@vegaprotocol/ui-toolkit';
const generate = pseudoRandom(1);
export const SSRLoader = () => { export const SSRLoader = () => {
const randomDelay = () => {
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
};
return ( return (
<div <div
style={{ style={{
@@ -38,9 +38,10 @@ export const SSRLoader = () => {
width: 10, width: 10,
height: 10, height: 10,
animation: 'flickering 0.4s linear alternate infinite', animation: 'flickering 0.4s linear alternate infinite',
animationDelay: `-${generate()}s`, animationDelay: `-${randomDelay()}s`,
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate', animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
background: 'black', background: 'black',
opacity: Math.random() > 0.5 ? 1 : 0,
}} }}
/> />
); );
@@ -7,7 +7,6 @@ import {
useEthTransactionUpdater, useEthTransactionUpdater,
useEthWithdrawApprovalsManager, useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3'; } from '@vegaprotocol/web3';
import { useLedgerDownloadManager } from '@vegaprotocol/ledger';
export const TransactionHandlers = () => { export const TransactionHandlers = () => {
useVegaTransactionManager(); useVegaTransactionManager();
@@ -15,6 +14,5 @@ export const TransactionHandlers = () => {
useEthTransactionManager(); useEthTransactionManager();
useEthTransactionUpdater(); useEthTransactionUpdater();
useEthWithdrawApprovalsManager(); useEthWithdrawApprovalsManager();
useLedgerDownloadManager();
return null; return null;
}; };
@@ -45,28 +45,6 @@ const secondRow = {
} as AccountFields; } as AccountFields;
const multiRowData = [singleRow, secondRow]; const multiRowData = [singleRow, secondRow];
const zeroBalanceRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
balance: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
decimals: 5,
},
available: '0',
used: '0',
total: '0',
} as AccountFields;
const zeroBalanceRowData = [zeroBalanceRow];
const onClickDepositMock = jest.fn();
describe('AccountsTable', () => { describe('AccountsTable', () => {
it('should render correct columns', async () => { it('should render correct columns', async () => {
// 7001-COLL-001 // 7001-COLL-001
@@ -93,27 +71,6 @@ describe('AccountsTable', () => {
).toEqual(expectedHeaders); ).toEqual(expectedHeaders);
}); });
it('should render deposit button', async () => {
render(
<AccountTable
rowData={zeroBalanceRowData}
onClickAsset={() => null}
isReadOnly={false}
onClickDeposit={onClickDepositMock}
pinnedAsset={{
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC',
}}
/>
);
const depositButton = screen.getByTestId('deposit');
expect(depositButton).toBeVisible();
await userEvent.click(depositButton);
expect(onClickDepositMock).toHaveBeenCalled();
});
it('should apply correct formatting', async () => { it('should apply correct formatting', async () => {
const { container } = render( const { container } = render(
<AccountTable <AccountTable
@@ -88,9 +88,6 @@ describe('TransferForm', () => {
}); });
it('validates a manually entered address', async () => { it('validates a manually entered address', async () => {
// 1003-TRAN-012
// 1003-TRAN-013
// 1003-TRAN-004
render(<TransferForm {...props} />); render(<TransferForm {...props} />);
submit(); submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); expect(await screen.findAllByText('Required')).toHaveLength(3);
@@ -119,11 +116,6 @@ describe('TransferForm', () => {
}); });
it('validates fields and submits', async () => { it('validates fields and submits', async () => {
// 1003-TRAN-002
// 1003-TRAN-003
// 1002-WITH-010
// 1003-TRAN-011
// 1003-TRAN-014
render(<TransferForm {...props} />); render(<TransferForm {...props} />);
// check current pubkey not shown // check current pubkey not shown
+4 -25
View File
@@ -4,10 +4,9 @@ import {
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { AssetsDocument } from './__generated__/Assets'; import { AssetsDocument } from './__generated__/Assets';
import { AssetStatus } from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type { AssetsQuery } from './__generated__/Assets'; import type { AssetsQuery } from './__generated__/Assets';
import type { Asset } from './asset-data-provider'; import type { Asset } from './asset-data-provider';
import { DENY_LIST } from './constants';
export interface BuiltinAssetSource { export interface BuiltinAssetSource {
__typename: 'BuiltinAsset'; __typename: 'BuiltinAsset';
@@ -49,7 +48,6 @@ export const assetsMapProvider = makeDerivedDataProvider<
} }
); );
/** Returns a record of assets by id */
export const useAssetsMapProvider = () => export const useAssetsMapProvider = () =>
useDataProvider({ useDataProvider({
dataProvider: assetsMapProvider, dataProvider: assetsMapProvider,
@@ -60,32 +58,13 @@ export const enabledAssetsProvider = makeDerivedDataProvider<
ReturnType<typeof getData>, ReturnType<typeof getData>,
never never
>([assetsProvider], ([assets]) => >([assetsProvider], ([assets]) =>
(assets as ReturnType<typeof getData>) (assets as ReturnType<typeof getData>).filter(
.filter((a) => a.status === AssetStatus.STATUS_ENABLED) (a) => a.status === Schema.AssetStatus.STATUS_ENABLED
.filter((a) => { )
const env = process.env['NX_VEGA_ENV'];
if (!env || !DENY_LIST[env]) return true;
if (DENY_LIST[env].includes(a.id)) {
return false;
}
return true;
})
); );
/** Returns all assets */
export const useAssetsDataProvider = () => export const useAssetsDataProvider = () =>
useDataProvider({ useDataProvider({
dataProvider: assetsProvider, dataProvider: assetsProvider,
variables: undefined, variables: undefined,
}); });
/** Returns assets that are enabled and are not on the deny list */
export const useEnabledAssets = () => {
return useDataProvider({
dataProvider: enabledAssetsProvider,
variables: undefined,
});
};
-63
View File
@@ -1,67 +1,4 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t( export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them" "The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
); );
// List of defunct and no longer used assets that were created for various testnets
export const DENY_LIST: Record<string, string[]> = {
TESTNET: [
'07f8ce4e3df6c649f3fd794112f545ede9fbd9cfa76a9e380a7502a8d525e4db',
'0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
'169fd651dd1303fc6f3e1dee8349439140cf467a190bc9a04bff3e617509cfd4',
'16ae5dbb1fd7aa2ddef725703bfe66b3647a4da7b844bfdd04e985756f53d9d6',
'177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
'20ffe5b1f70364a6b976007d96d306345bc7b880256ddffbbea2c590096ef375',
'2792b6ead78a812bd0169b2adbe9c2ea5b8c21510e40b93bf8d2cba6a0838e90',
'27c0229ab9edc6a3ba6a3e9716c1b324d6515a3168004256879e2de7c240f453',
'285923fed8c66ffb416b163e8ec72d3a87b9b8e2570e7ee7fe97d7092a918bc8',
'2af02165ef5ea1d1284b4251117eaae45faa961ec918e3f69b66903a18822387',
'2bf406160d06280d92412605eb397d08142736f4e17967e404dc892da278e9da',
'373d90d97bd3f97f0c13cf449926cfd7f1ed5f3746c3091e8dd894d60bf5eac8',
'3c7d6bd83dac3174923ba65f3af01f42858e267ca5b722fe2b0b3fec14e98ec2',
'434b502cabb7a9a36d3036a599d589537da293d8bbab224692629351a7926249',
'43bb50d6ba0a871088014cb204079ace4811970ce3d52b15896f6e7a88f900b7',
'46e9d23142762db57751b1b507e15747fe9478de504cbc4a0bffd828c39e3776',
'4b6957c6a184b002db80c7a4dd811d0e70866be4a4c8fffa4894e78aa132b407',
'4cbe1ad4cf232fb2232176e3f440f6e6fdc3ba7b1d95a99cb69116b9e379eae8',
'4e4e80abff30cab933b8c4ac6befc618372eb76b2cbddc337eff0b4a3a4d25b8',
'51eed0c17e3287996ba6006ce56de2591550ebfad46631f3a0129886967420e1',
'522e09779d984ae77b60036f4ecd80f0491f5f0c8cc386a0712b78a020ab62e6',
'5c950d4cfe1aaf9174c4358ee5df03f7e6bc556eac06a012cf6c07db154e20c4',
'5d8eb4e4e385b85c5c11f47111f315d5d201e04f4ef5e294763bf51036974b91',
'5dd5c226e944b4c3df3e8dbbbc473e72fb134f4c5b82a1968cc3cf7c1b40fe9e',
'638b4dbe4f3ce5b81546bef29108e8374a7293012c6189ca00f287f8c62a98a5',
'644c3a6e258382542a108c4dca16fae93657b6ff40238b2bb55854a125316052',
'71ea74ef1aa7582bcdd4ef843b3e79584ded4d16e692f3946cac4c9d61aed4f2',
'72f051ea66686ae004086f1ad086866f720f25896319abf3427cae101a58d985',
'827bbacde0dd5bfec857933dbbf642147f2afe6a9db0a543839b1b5239ca6786',
'83f4970aa1aeab85d01d8a976cbfd0fb3afedf391e18197e8c94c4021b3dffa1',
'84a20b554854c9af10ef84d308795dfae455834d1a207798292d66e19fd72c92',
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f',
'85771250f0aa84727a1c01ba8926e59217ca13a7706ba03cf2ad95abbbc2e3bc',
'8a246b15bf185065d3f41a710f3eea0fbbe8754a8464501cf4a6d253d69c6423',
'8e5553ee4e95db51523640d1d03b00c8ab31dd69d787e169b0d958ef52e19a0a',
'92747340d922e9cda5bde17c46af8b409e47d6342f3b9c8fb058a34c6206d373',
'9b43c6791aebed5f7b38d91907539173aa278de88294b6422f3dd8282cd2c6e2',
'9e0bb9bd7ea2ec51efcdc98b432b6f0b055b2ed7973cfac9b44899d6e6c5deab',
'a0cf0835cb5af7bd03c56d12d846eed81c182eab36ea61c074961c115da9c8c4',
'a26dc4f6dea91e80d42e4736da7fe3c42f6275df9c4dca48be3ef96850192242',
'a4c945aea50ef321eabf8a3bcf0b8bb74288452c4638876ee4df5fb59be4a0ec',
'ae28c1043e79bdb95cae0e9919d4436157d14b183e24ab4572793812f3d89222',
'b0cf8be4e4d1d2ac5156c2298aa42edc7a74d25b7838d06e2279e94018a47f3f',
'b6d11705cb456768ff8271074dd8adcd78bd042a27fc205e9b52f9f945f42ea1',
'be478570584211521890fb65a41bded4d2bf29beb0b61ac3cc3b97f828a27b22',
'befde6d78ea1c996ae1d959342730220e274992097aeeae00c908a971314e6ae',
'bf5b9464d154ffeff57f05f90b223c1045f0754befd01a9a42c99382115e7833',
'd2239a7c92bc5c72a959a73ccee7727ba0966f4d88a33d71dbbc26e3f171555d',
'd3f9cf6e1711595b4588366a96b725fc039f72a8a28b2e4c1b630016dd2eed39',
'd9a9ab00bb79ce7dca7f6329ab627dcea2e53e5c16b85bf4aa8c3980b76fad57',
'dd73f7d1e721ef5cba95f454a3019521456c1b43b97084ad29e3e6509148d45d',
'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
'ede4076aef07fd79502d14326c54ab3911558371baaf697a19d077f4f89de399',
'f481abe5700c6940ca996adfa9d07c9d9f07a486a7ce9cdb2e6ee0fa6450fe31',
'f7355ec52730c2e6ba02216da8501b4b6934cd363511e9d47ae5b97056d02703',
'fdf0ec118d98393a7702cf72e46fc87ad680b152f64b2aac59e093ac2d688fbb',
],
};
@@ -37,7 +37,6 @@ import {
AccordionChevron, AccordionChevron,
AccordionPanel, AccordionPanel,
Intent, Intent,
ExternalLink,
Pill, Pill,
Tooltip, Tooltip,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
@@ -386,21 +385,7 @@ export const DealTicketMarginDetails = ({
value={liquidationPriceEstimateRange} value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
symbol={quoteName} symbol={quoteName}
labelDescription={ labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
<>
<span>{LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}</span>{' '}
<span>
{t('For full details please see ')}
<ExternalLink
href={
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
}
>
{t('liquidation price estimate documentation.')}
</ExternalLink>
</span>
</>
}
/> />
{partyId && ( {partyId && (
<AccountBreakdownDialog <AccountBreakdownDialog
+1 -1
View File
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
); );
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t( export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.' 'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
); );
export const EST_SLIPPAGE = t( export const EST_SLIPPAGE = t(
+6 -2
View File
@@ -1,14 +1,18 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment'; import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit'; import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
import { DepositManager } from './deposit-manager'; import { DepositManager } from './deposit-manager';
import { useEnabledAssets } from '@vegaprotocol/assets'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { enabledAssetsProvider } from '@vegaprotocol/assets';
/** /**
* Fetches data required for the Deposit page * Fetches data required for the Deposit page
*/ */
export const DepositContainer = ({ assetId }: { assetId?: string }) => { export const DepositContainer = ({ assetId }: { assetId?: string }) => {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const { data, error, loading } = useEnabledAssets(); const { data, loading, error } = useDataProvider({
dataProvider: enabledAssetsProvider,
variables: undefined,
});
return ( return (
<AsyncRendererInline data={data} loading={loading} error={error}> <AsyncRendererInline data={data} loading={loading} error={error}>
+1 -1
View File
@@ -148,7 +148,7 @@ describe('Deposit form', () => {
}); });
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
// 1001-DEPO-004
expect( expect(
await screen.findByText( await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5" "You can't deposit more than you have in your Ethereum wallet, 5"
@@ -6,12 +6,8 @@ fragment FundingPaymentFields on FundingPayment {
timestamp timestamp
} }
query FundingPayments($partyId: ID!, $pagination: Pagination, $marketId: ID) { query FundingPayments($partyId: ID!, $pagination: Pagination) {
fundingPayments( fundingPayments(partyId: $partyId, pagination: $pagination) {
partyId: $partyId
pagination: $pagination
marketId: $marketId
) {
edges { edges {
node { node {
...FundingPaymentFields ...FundingPaymentFields
@@ -8,7 +8,6 @@ export type FundingPaymentFieldsFragment = { __typename?: 'FundingPayment', mark
export type FundingPaymentsQueryVariables = Types.Exact<{ export type FundingPaymentsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>; pagination?: Types.InputMaybe<Types.Pagination>;
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
}>; }>;
@@ -24,8 +23,8 @@ export const FundingPaymentFieldsFragmentDoc = gql`
} }
`; `;
export const FundingPaymentsDocument = gql` export const FundingPaymentsDocument = gql`
query FundingPayments($partyId: ID!, $pagination: Pagination, $marketId: ID) { query FundingPayments($partyId: ID!, $pagination: Pagination) {
fundingPayments(partyId: $partyId, pagination: $pagination, marketId: $marketId) { fundingPayments(partyId: $partyId, pagination: $pagination) {
edges { edges {
node { node {
...FundingPaymentFields ...FundingPaymentFields
@@ -56,7 +55,6 @@ export const FundingPaymentsDocument = gql`
* variables: { * variables: {
* partyId: // value for 'partyId' * partyId: // value for 'partyId'
* pagination: // value for 'pagination' * pagination: // value for 'pagination'
* marketId: // value for 'marketId'
* }, * },
* }); * });
*/ */
@@ -8,14 +8,12 @@ import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provi
interface FundingPaymentsManagerProps { interface FundingPaymentsManagerProps {
partyId: string; partyId: string;
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
gridProps: ReturnType<typeof useDataGridEvents>; gridProps: ReturnType<typeof useDataGridEvents>;
} }
export const FundingPaymentsManager = ({ export const FundingPaymentsManager = ({
partyId, partyId,
marketId,
onMarketClick, onMarketClick,
gridProps, gridProps,
}: FundingPaymentsManagerProps) => { }: FundingPaymentsManagerProps) => {
@@ -29,7 +27,7 @@ export const FundingPaymentsManager = ({
} }
return false; return false;
}, },
variables: { partyId, marketId }, variables: { partyId },
}); });
return ( return (
-1
View File
@@ -1,2 +1 @@
export * from './lib/ledger-export-form'; export * from './lib/ledger-export-form';
export * from './lib/ledger-download-store';
@@ -1,142 +0,0 @@
import { create } from 'zustand';
import type { ReactNode } from 'react';
import { useCallback, useEffect } from 'react';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { useToasts, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { subscribeWithSelector } from 'zustand/middleware';
type DownloadSettings = {
title: string;
link: string;
filename?: string;
isDownloaded?: boolean;
isChanged?: boolean;
isError?: boolean;
errorMessage?: string;
isDelayed?: boolean;
intent?: Intent;
blob?: Blob;
};
export type LedgerDownloadFileStore = {
queue: DownloadSettings[];
hasItem: (link: string) => boolean;
removeItem: (link: string) => void;
updateQueue: (item: DownloadSettings) => void;
};
export const useLedgerDownloadFile = create<LedgerDownloadFileStore>()(
subscribeWithSelector((set, get) => ({
queue: [],
hasItem: (link: string) =>
get().queue.findIndex((item) => item.link === link) > -1,
removeItem: (link: string) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === link);
if (index > -1) {
queue.splice(index, 1);
set({ queue: [...queue] });
}
},
updateQueue: (newitem: DownloadSettings) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === newitem.link);
if (index > -1) {
queue[index] = { ...queue[index], ...newitem };
set({ queue: [...queue] });
} else {
set({ queue: [newitem, ...queue] });
}
},
}))
);
const ErrorContent = ({ message }: { message?: string }) => (
<>
<h4 className="mb-1 text-sm">{t('Something went wrong')}</h4>
<p>{message || t('Try again later')}</p>
</>
);
const InfoContent = ({ progress = false }) => (
<>
<p>{t('Please note this can take several minutes.')}</p>
<p>{t('You will be notified here when your file is ready.')}</p>
<h4 className="my-2">
{progress ? t('Still in progress') : t('Download has been started')}
</h4>
</>
);
export const useLedgerDownloadManager = () => {
const queue = useLedgerDownloadFile((store) => store.queue);
const updateQueue = useLedgerDownloadFile((store) => store.updateQueue);
const removeItem = useLedgerDownloadFile((store) => store.removeItem);
const [setToast, updateToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.update,
store.hasToast,
store.remove,
]);
const onDownloadClose = useCallback(
(id: string) => {
removeToast(id);
removeItem(id);
},
[removeToast, removeItem]
);
const createToast = (item: DownloadSettings) => {
let content: ReactNode;
switch (true) {
case item.isError:
content = <ErrorContent message={item.errorMessage} />;
break;
case Boolean(item.blob):
content = (
<>
<h4 className="mb-1 text-sm">{t('Your file is ready')}</h4>
<a
onClick={() => onDownloadClose(item.link)}
href={URL.createObjectURL(item.blob as Blob)}
download={item.filename}
className="underline"
>
{t('Get file here')}
</a>
</>
);
break;
default:
content = <InfoContent progress={item.isDelayed} />;
}
const toast: Toast = {
id: item.link,
intent: item.intent || Intent.Primary,
content: (
<>
<h3 className="mb-1 text-md uppercase">{item.title}</h3>
{content}
</>
),
onClose: () => onDownloadClose(item.link),
loader: !item.isDownloaded && !item.isError,
};
if (hasToast(toast.id)) {
updateToast(toast.id, toast);
} else {
setToast(toast);
}
};
useEffect(() => {
queue.forEach((item) => {
if (item.isChanged) {
createToast(item);
updateQueue({ ...item, isChanged: false });
}
});
}, [queue, createToast, updateQueue]);
};
+20 -56
View File
@@ -1,21 +1,10 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form'; import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils'; import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
import {
useLedgerDownloadManager,
useLedgerDownloadFile,
} from './ledger-download-store';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetToast = jest.fn();
jest.mock('@vegaprotocol/ui-toolkit', () => ({
...jest.requireActual('@vegaprotocol/ui-toolkit'),
useToasts: jest.fn(() => [mockSetToast, jest.fn(), jest.fn(() => false)]),
}));
const vegaUrl = 'https://vega-url.co.uk/querystuff'; const vegaUrl = 'https://vega-url.co.uk/querystuff';
const mockResponse = { const mockResponse = {
ok: true,
headers: { get: jest.fn() }, headers: { get: jest.fn() },
blob: () => '', blob: () => '',
}; };
@@ -39,7 +28,6 @@ describe('LedgerExportForm', () => {
afterAll(() => { afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
jest.clearAllMocks();
}); });
it('should be properly rendered', async () => { it('should be properly rendered', async () => {
@@ -55,6 +43,8 @@ describe('LedgerExportForm', () => {
// userEvent does not work with faked timers // userEvent does not work with faked timers
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -62,6 +52,9 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000` }&dateRange.startTimestamp=1691057410000000000`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('assetID should be properly change request url', async () => { it('assetID should be properly change request url', async () => {
@@ -82,6 +75,8 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -89,6 +84,9 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000` }&dateRange.startTimestamp=1691057410000000000`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('date-from should properly change request url', async () => { it('date-from should properly change request url', async () => {
@@ -112,6 +110,8 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -119,6 +119,10 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}` }&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('date-to should properly change request url', async () => { it('date-to should properly change request url', async () => {
@@ -152,6 +156,10 @@ describe('LedgerExportForm', () => {
)}` )}`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('Time zone sentence should be properly displayed', () => { it('Time zone sentence should be properly displayed', () => {
@@ -197,50 +205,6 @@ describe('LedgerExportForm', () => {
screen.queryByText(/^The downloaded file uses the UTC/) screen.queryByText(/^The downloaded file uses the UTC/)
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('A toast notification should be displayed', async () => {
useLedgerDownloadFile.setState({ queue: [] });
const TestWrapper = () => {
useLedgerDownloadManager();
return (
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
};
render(<TestWrapper />);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ledger-download-button'));
const link = `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000`;
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalledWith({
id: link,
content: expect.any(Object),
onClose: expect.any(Function),
intent: Intent.Primary,
loader: true,
});
expect(global.fetch).toHaveBeenCalledWith(link);
});
mockSetToast.mockClear();
(global.fetch as jest.Mock).mockClear();
fireEvent.click(screen.getByTestId('ledger-download-button')); // click again
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
});
});
}); });
describe('createDownloadUrl', () => { describe('createDownloadUrl', () => {
+31 -75
View File
@@ -1,22 +1,17 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { format, subDays } from 'date-fns'; import { z } from 'zod';
import { import {
Intent,
Loader,
TradingButton, TradingButton,
Loader,
TradingFormGroup, TradingFormGroup,
TradingInput, TradingInput,
TradingSelect, TradingSelect,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { z } from 'zod'; import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils';
import {
formatForInput,
toNanoSeconds,
VEGA_ID_REGEX,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger'; import { localLoggerFactory } from '@vegaprotocol/logger';
import { useLedgerDownloadFile } from './ledger-download-store'; import { formatForInput } from '@vegaprotocol/utils';
import { subDays } from 'date-fns';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv'; const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
@@ -78,14 +73,10 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const maxFromDate = formatForInput(new Date(dateTo || now.current)); const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current); const maxToDate = formatForInput(now.current);
const [isDownloading, setIsDownloading] = useState(false);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]); const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl); const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId); const disabled = Boolean(!assetId || isDownloading);
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
const updateDownloadQueue = useLedgerDownloadFile(
(store) => store.updateQueue
);
const assetDropDown = ( const assetDropDown = (
<TradingSelect <TradingSelect
@@ -96,6 +87,7 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
}} }}
className="w-full" className="w-full"
data-testid="select-ledger-asset" data-testid="select-ledger-asset"
disabled={isDownloading}
> >
{Object.keys(assets).map((assetKey) => ( {Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}> <option key={assetKey} value={assetKey}>
@@ -105,78 +97,32 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
</TradingSelect> </TradingSelect>
); );
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => { const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const title = t('Downloading for %s from %s till %s', [
assets[assetId],
format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
]);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
});
}, 1000 * 30);
try { try {
updateDownloadQueue(downloadStoreItem); const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
setIsDownloading(true);
const resp = await fetch(link); const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp; const { headers } = resp;
const nameHeader = headers.get('content-disposition'); const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME; const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob(); const blob = await resp.blob();
if (blob) { if (blob) {
updateDownloadQueue({ const link = document.createElement('a');
...downloadStoreItem, link.href = URL.createObjectURL(blob);
blob, link.download = filename;
isDownloaded: true, link.click();
isChanged: true,
intent: Intent.Success,
});
} }
} catch (err) { } catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err); localLoggerFactory({ application: 'ledger' }).error('Download file', err);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally { } finally {
clearTimeout(ts); setIsDownloading(false);
} }
}; };
@@ -199,6 +145,7 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-from" id="date-from"
value={dateFrom} value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)} onChange={(e) => setDateFrom(e.target.value)}
disabled={disabled}
max={maxFromDate} max={maxFromDate}
/> />
</TradingFormGroup> </TradingFormGroup>
@@ -209,10 +156,19 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-to" id="date-to"
value={dateTo} value={dateTo}
onChange={(e) => setDateTo(e.target.value)} onChange={(e) => setDateTo(e.target.value)}
disabled={disabled}
max={maxToDate} max={maxToDate}
/> />
</TradingFormGroup> </TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}> <div className="relative text-sm" title={t('Download all to .csv file')}>
{isDownloading && (
<div
className="absolute flex items-center justify-center w-full h-full"
data-testid="download-spinner"
>
<Loader size="small" />
</div>
)}
<TradingButton <TradingButton
fill fill
disabled={disabled} disabled={disabled}
@@ -128,26 +128,6 @@ export const NetworkParams = {
'governance_proposal_VolumeDiscountProgram_requiredMajority', 'governance_proposal_VolumeDiscountProgram_requiredMajority',
governance_proposal_VolumeDiscountProgram_requiredParticipation: governance_proposal_VolumeDiscountProgram_requiredParticipation:
'governance_proposal_VolumeDiscountProgram_requiredParticipation', 'governance_proposal_VolumeDiscountProgram_requiredParticipation',
governance_proposal_transfer_maxAmount:
'governance_proposal_transfer_maxAmount',
governance_proposal_transfer_maxClose:
'governance_proposal_transfer_maxClose',
governance_proposal_transfer_maxEnact:
'governance_proposal_transfer_maxEnact',
governance_proposal_transfer_maxFraction:
'governance_proposal_transfer_maxFraction',
governance_proposal_transfer_minClose:
'governance_proposal_transfer_minClose',
governance_proposal_transfer_minEnact:
'governance_proposal_transfer_minEnact',
governance_proposal_transfer_minProposerBalance:
'governance_proposal_transfer_minProposerBalance',
governance_proposal_transfer_minVoterBalance:
'governance_proposal_transfer_minVoterBalance',
governance_proposal_transfer_requiredMajority:
'governance_proposal_transfer_requiredMajority',
governance_proposal_transfer_requiredParticipation:
'governance_proposal_transfer_requiredParticipation',
validators_delegation_minAmount: 'validators_delegation_minAmount', validators_delegation_minAmount: 'validators_delegation_minAmount',
spam_protection_minimumWithdrawalQuantumMultiple: spam_protection_minimumWithdrawalQuantumMultiple:
'spam_protection_minimumWithdrawalQuantumMultiple', 'spam_protection_minimumWithdrawalQuantumMultiple',
+2 -8
View File
@@ -104,13 +104,7 @@ describe('TradesTable', () => {
const [firstDateCell, secondDateCell] = createdAtCells; const [firstDateCell, secondDateCell] = createdAtCells;
// Compare the times as strings // Compare the times as strings
// Note: I've changed it form literal strings to this because of the expect(firstDateCell.textContent).toBe('6:59:00 PM');
// locale mismatch. Mocking the format also resulted with strange "space" diff. expect(secondDateCell.textContent).toBe('7:00:00 PM');
expect(firstDateCell.textContent).toBe(
getTimeFormat().format(new Date(trade2.createdAt))
);
expect(secondDateCell.textContent).toBe(
getTimeFormat().format(new Date(trade.createdAt))
);
}); });
}); });
+7 -20
View File
@@ -4605,8 +4605,6 @@ export type QueryreferralFeeStatsArgs = {
assetId?: InputMaybe<Scalars['ID']>; assetId?: InputMaybe<Scalars['ID']>;
epoch?: InputMaybe<Scalars['Int']>; epoch?: InputMaybe<Scalars['Int']>;
marketId?: InputMaybe<Scalars['ID']>; marketId?: InputMaybe<Scalars['ID']>;
referee?: InputMaybe<Scalars['ID']>;
referrer?: InputMaybe<Scalars['ID']>;
}; };
@@ -4622,9 +4620,9 @@ export type QueryreferralSetRefereesArgs = {
/** Queries allow a caller to read data and filter data via GraphQL. */ /** Queries allow a caller to read data and filter data via GraphQL. */
export type QueryreferralSetStatsArgs = { export type QueryreferralSetStatsArgs = {
epoch?: InputMaybe<Scalars['Int']>; epoch?: InputMaybe<Scalars['Int']>;
id: Scalars['ID'];
pagination?: InputMaybe<Pagination>; pagination?: InputMaybe<Pagination>;
partyId?: InputMaybe<Scalars['ID']>; partyId?: InputMaybe<Scalars['ID']>;
setId?: InputMaybe<Scalars['ID']>;
}; };
@@ -4884,10 +4882,6 @@ export type ReferralSetStats = {
referralSetRunningNotionalTakerVolume: Scalars['String']; referralSetRunningNotionalTakerVolume: Scalars['String'];
/** Reward factor applied to the party. */ /** Reward factor applied to the party. */
rewardFactor: Scalars['String']; rewardFactor: Scalars['String'];
/** The proportion of the referees taker fees to be rewarded to the referrer. */
rewardsFactorMultiplier: Scalars['String'];
/** The multiplier applied to the referral reward factor when calculating referral rewards due to the referrer. */
rewardsMultiplier: Scalars['String'];
}; };
/** Connection type for retrieving cursor-based paginated referral set statistics information */ /** Connection type for retrieving cursor-based paginated referral set statistics information */
@@ -6149,18 +6143,7 @@ export type UpdateProductConfiguration = UpdateFutureProduct | UpdatePerpetualPr
export type UpdateReferralProgram = { export type UpdateReferralProgram = {
__typename?: 'UpdateReferralProgram'; __typename?: 'UpdateReferralProgram';
/** Defined tiers in increasing order. First element will give Tier 1, second element will give Tier 2, etc. */ changes: ReferralProgram;
benefitTiers: Array<BenefitTier>;
/** Timestamp as RFC3339, after which when the current epoch ends, the programs will end and benefits will be disabled. */
endOfProgramTimestamp: Scalars['String'];
/**
* Defined staking tiers in increasing order. First element will give Tier 1,
* second element will give Tier 2, and so on. Determines the level of
* benefit a party can expect based on their staking.
*/
stakingTiers: Array<StakingTier>;
/** Number of epochs over which to evaluate a referral set's running volume. */
windowLength: Scalars['Int'];
}; };
/** Update an existing spot market on Vega */ /** Update an existing spot market on Vega */
@@ -6192,7 +6175,11 @@ export type UpdateVolumeDiscountProgram = {
benefitTiers: Array<VolumeBenefitTier>; benefitTiers: Array<VolumeBenefitTier>;
/** The end time of the program */ /** The end time of the program */
endOfProgramTimestamp: Scalars['Timestamp']; endOfProgramTimestamp: Scalars['Timestamp'];
/** The window length to consider for the volume discount program */ /** ID of the proposal that created the discount program */
id: Scalars['ID'];
/** The current version of the volume discount program */
version: Scalars['Int'];
/** The window legnth to consider for the volume discount program */
windowLength: Scalars['Int']; windowLength: Scalars['Int'];
}; };
@@ -1,7 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useMemo } from 'react';
import styles from './loader.module.scss'; import styles from './loader.module.scss';
export const pseudoRandom = (seed: number) => { const pseudoRandom = (seed: number) => {
let value = seed; let value = seed;
return () => { return () => {
value = (value * 16807) % 2147483647; value = (value * 16807) % 2147483647;
@@ -9,8 +10,6 @@ export const pseudoRandom = (seed: number) => {
}; };
}; };
const generate = pseudoRandom(1);
export interface LoaderProps { export interface LoaderProps {
size?: 'small' | 'large'; size?: 'small' | 'large';
forceTheme?: 'dark' | 'light'; forceTheme?: 'dark' | 'light';
@@ -28,6 +27,8 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]'; size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
const items = size === 'small' ? 9 : 25; const items = size === 'small' ? 9 : 25;
const generate = useMemo(() => pseudoRandom(1), []);
return ( return (
<div className="flex flex-col items-center" data-testid="loader"> <div className="flex flex-col items-center" data-testid="loader">
<div className={`${wrapperClasses} flex flex-wrap`}> <div className={`${wrapperClasses} flex flex-wrap`}>
+1 -9
View File
@@ -8,13 +8,5 @@ When I enter on ledger entries tab in portfolio page
- in the form **Must** see a dropdown for select an asset, in which reports will be downloaded (<a name="7007-LEEN-002" href="#7007-LEEN-002">7007-LEEN-002</a>) - in the form **Must** see a dropdown for select an asset, in which reports will be downloaded (<a name="7007-LEEN-002" href="#7007-LEEN-002">7007-LEEN-002</a>)
- in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>) - in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>)
- default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>) - default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>)
- during download a loader component **Must** be visible and all interactive elements in the form **Must** be disabled (<a name="7007-LEEN-005" href="#7007-LEEN-005">7007-LEEN-005</a>)
- **Must** see a note about time in file are in UTC and timezone of the user relative to UTC (<a name="7007-LEEN-006" href="#7007-LEEN-006">7007-LEEN-006</a>) - **Must** see a note about time in file are in UTC and timezone of the user relative to UTC (<a name="7007-LEEN-006" href="#7007-LEEN-006">7007-LEEN-006</a>)
- As a user, I **must** see a message saying that this can take several minutes (<a name="7007-LEEN-007" href="#7007-LEEN-007">7007-LEEN-007</a>)
- After half a minute, the message is updated to say something like 'Still in progress' (<a name="7007-LEEN-008" href="#7007-LEEN-008">7007-LEEN-008</a>)
- A toast is shown when the download is complete (<a name="7007-LEEN-009" href="#7007-LEEN-009">7007-LEEN-009</a>)
- The download button should never be disabled
- If user tries to download file which is already in download: (<a name="7007-LEEN-010" href="#7007-LEEN-010">7007-LEEN-010</a>)
- if notification stayed open, nothing happens
- If notification was closed, will be open, no any new request will be fired
- If something has changed in the form (asset, dates, `Date.now`) new download will start.
- The state of the download form should be in sync with the download itself if you navigate away from the page or reload (<a name="7007-LEEN-011" href="#7007-LEEN-011">7007-LEEN-011</a>)