Compare commits

..
Author SHA1 Message Date
Madalina Raicu cf7db57565 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/order-stopped-intent-toasts 2023-04-03 19:41:01 +01:00
Madalina Raicu 83c97ac0ff fix: add reduce and post only tests 2023-04-03 18:49:54 +01:00
Madalina Raicu d21a328f81 fix: update tooltip 2023-04-03 18:36:31 +01:00
Madalina Raicu b449d2671a fix: reduce only disable on persistent orders 2023-04-03 18:33:27 +01:00
Madalina Raicu 6f48fd2034 fix: update deal ticket postOnly selection 2023-04-03 18:05:17 +01:00
Madalina Raicu e753a77fb6 fix: reset post only and reduce only when changing TIF and type 2023-04-03 17:32:08 +01:00
Madalina Raicu 319edacbfc fix: order list text translation 2023-04-03 16:11:43 +01:00
Madalina Raicu a3308b9966 fix: remove one log 2023-04-03 16:01:50 +01:00
Madalina Raicu 1078c15f95 fix: order list rejected sjowing undefined 2023-04-03 15:29:33 +01:00
Madalina Raicu 8c856e482c fix: update reduce/post only labels 2023-04-03 14:49:12 +01:00
Madalina Raicu f0d3cb9ca2 Merge branch 'feat/3248-total-transfer-amount-fee' of github.com:vegaprotocol/frontend-monorepo into fix/order-stopped-intent-toasts 2023-04-03 14:32:28 +01:00
Madalina Raicu dedd81dd1c feat(trading): show total amount plus transfer fee 2023-04-03 14:31:20 +01:00
Madalina Raicu 0bc94efc72 fix: show reduce/post only in order list 2023-04-03 14:25:31 +01:00
Madalina Raicu 5b015858f2 fix: fix intent on rder stopped 2023-04-03 14:16:11 +01:00
Madalina Raicu f1b5728a17 fix: update post only on disabled to be false 2023-04-03 14:02:23 +01:00
Madalina Raicu 92293bfd98 fix: order rejection appearance 2023-04-03 13:35:16 +01:00
Madalina Raicu 1b1d1865f8 fix: remove redundant log 2023-04-03 12:29:59 +01:00
Madalina Raicu f781ae4ae1 feat(trading): show total amount plus transfer fee 2023-04-03 12:27:30 +01:00
Madalina Raicu b8dcbd2e44 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into develop 2023-04-03 10:09:46 +01:00
123 changed files with 4229 additions and 4114 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
NX_VEGA_NETWORKS='{"STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
+20
View File
@@ -0,0 +1,20 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_ENV=MIRROR
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
# App flags
NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
NX_EXPLORER_MARKETS=0
NX_EXPLORER_ORACLES=0
NX_EXPLORER_TXS_LIST=1
+12
View File
@@ -0,0 +1,12 @@
# App configuration variables
NX_VEGA_ENV=SANDBOX
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
+1 -1
View File
@@ -9,5 +9,5 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
+12
View File
@@ -0,0 +1,12 @@
# App configuration variables
NX_VEGA_ENV=MIRROR
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.n00.mainnet-mirror.vega.xyz/graphql
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://mirror.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_DELEGATIONS_PAGINATION=50
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+9
View File
@@ -0,0 +1,9 @@
# App configuration variables
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
NX_VEGA_ENV=SANDBOX
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -8,4 +8,3 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
+8 -4
View File
@@ -12,7 +12,6 @@ const TRUTHY = ['1', 'true'];
interface VegaContracts {
claimAddress: string;
lockedAddress: string;
tokenVestingAddress?: string;
}
const customClaimAddress = process.env['NX_CUSTOM_CLAIM_ADDRESS'] as string;
@@ -37,16 +36,21 @@ export const ContractAddresses: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
SANDBOX: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
TESTNET: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
MIRROR: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
VALIDATOR_TESTNET: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
// This is a fallback contract address for the validator testnet network which does not
// have a vesting contract address set and is therefore not in the ethereum config
tokenVestingAddress: '0xadFcb7f93a24F8743a8e548d74d2ecB373c92866',
},
MAINNET: {
claimAddress: '0x0ee1fb382caf98e86e97e51f9f42f8b4654020f3',
@@ -49,13 +49,6 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
signer = provider.getSigner();
}
const tokenVestingAddress =
config.token_vesting_contract?.address ||
ENV.addresses.tokenVestingAddress;
if (!tokenVestingAddress) {
throw new Error('No token vesting address found');
}
if (provider && config) {
const staking = new StakingBridge(
config.staking_bridge_contract.address,
@@ -70,7 +63,7 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
signer || provider
),
vesting: new TokenVesting(
tokenVestingAddress,
config.token_vesting_contract.address,
signer || provider
),
claim: new Claim(ENV.addresses.claimAddress, signer || provider),
@@ -7,10 +7,9 @@ query PreviousEpoch($epochId: ID) {
id
rewardScore {
rawValidatorScore
performanceScore
}
rankingScore {
stakeScore
performanceScore
}
}
}
@@ -8,7 +8,7 @@ export type PreviousEpochQueryVariables = Types.Exact<{
}>;
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string } | null, rankingScore: { __typename?: 'RankingScore', stakeScore: string } } } | null> | null } | null } };
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string } | null, rankingScore: { __typename?: 'RankingScore', performanceScore: string } } } | null> | null } | null } };
export const PreviousEpochDocument = gql`
@@ -21,10 +21,9 @@ export const PreviousEpochDocument = gql`
id
rewardScore {
rawValidatorScore
performanceScore
}
rankingScore {
stakeScore
performanceScore
}
}
}
@@ -81,10 +81,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
rewardScore: {
rawValidatorScore: '0.25',
performanceScore: '0.9998677767864936',
},
rankingScore: {
stakeScore: '0.2499583402766206',
performanceScore: '0.9998677767864936',
},
},
},
@@ -93,10 +92,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
rewardScore: {
rawValidatorScore: '0.3',
performanceScore: '1',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '1',
},
},
},
@@ -105,10 +103,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
rewardScore: {
rawValidatorScore: '0.35',
performanceScore: '0.999629748500531',
},
rankingScore: {
stakeScore: '0.2312',
performanceScore: '0.999629748500531',
},
},
},
@@ -10,6 +10,7 @@ import {
getFormattedPerformanceScore,
getLastEpochScoreAndPerformance,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -51,6 +52,7 @@ interface CanonisedConsensusNodeProps {
[ValidatorFields.STAKED_BY_OPERATOR]: string;
[ValidatorFields.PERFORMANCE_SCORE]: string;
[ValidatorFields.PERFORMANCE_PENALTY]: string;
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
[ValidatorFields.OVERSTAKING_PENALTY]: string;
[ValidatorFields.TOTAL_PENALTIES]: string;
[ValidatorFields.PENDING_STAKE]: string;
@@ -162,11 +164,14 @@ export const ConsensusValidatorsTable = ({
pendingUserStake,
userStakeShare,
}) => {
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
stakeScore: previousEpochStakeScore,
} = getLastEpochScoreAndPerformance(previousEpochData, id);
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
totalStake
);
return {
id,
@@ -179,7 +184,7 @@ export const ConsensusValidatorsTable = ({
[ValidatorFields.NORMALISED_VOTING_POWER]:
getNormalisedVotingPower(votingPower),
[ValidatorFields.UNNORMALISED_VOTING_POWER]:
getUnnormalisedVotingPower(previousEpochValidatorScore),
getUnnormalisedVotingPower(rawValidatorScore),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
@@ -189,19 +194,18 @@ export const ConsensusValidatorsTable = ({
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
previousEpochPerformanceScore
).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
previousEpochPerformanceScore
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
previousEpochValidatorScore,
previousEpochStakeScore
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
previousEpochValidatorScore,
previousEpochPerformanceScore,
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
),
@@ -7,6 +7,7 @@ import { BigNumber } from '../../../../lib/bignumber';
import {
getFormattedPerformanceScore,
getLastEpochScoreAndPerformance,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -81,21 +82,21 @@ export const StandbyPendingValidatorsTable = ({
pendingUserStake,
userStakeShare,
}) => {
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
stakeScore: previousEpochStakeScore,
} = getLastEpochScoreAndPerformance(previousEpochData, id);
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
totalStake
);
let individualStakeNeededForPromotion,
individualStakeNeededForPromotionDescription;
if (stakeNeededForPromotion && previousEpochPerformanceScore) {
if (stakeNeededForPromotion && performanceScore) {
const stakedTotalBigNum = new BigNumber(stakedTotal);
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
const performanceScoreBigNum = new BigNumber(
previousEpochPerformanceScore
);
const performanceScoreBigNum = new BigNumber(performanceScore);
const calc = stakeNeededBigNum
.dividedBy(performanceScoreBigNum)
@@ -141,19 +142,18 @@ export const StandbyPendingValidatorsTable = ({
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
previousEpochPerformanceScore
).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
previousEpochPerformanceScore
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
previousEpochValidatorScore,
previousEpochStakeScore
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
previousEpochValidatorScore,
previousEpochPerformanceScore,
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
),
@@ -20,6 +20,7 @@ import { SubHeading } from '../../../components/heading';
import {
getLastEpochScoreAndPerformance,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -74,9 +75,15 @@ export const ValidatorTable = ({
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
const { rawValidatorScore, performanceScore, stakeScore } =
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, node.id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
node.stakedTotal
);
const stakePercentage = getStakePercentage(total, stakedOnNode);
const totalPenaltiesAmount = getTotalPenalties(
@@ -238,7 +245,7 @@ export const ValidatorTable = ({
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{getOverstakingPenalty(rawValidatorScore, stakeScore)}
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</Tooltip>
</KeyValueTableRow>
@@ -4,6 +4,7 @@ import {
getNormalisedVotingPower,
getUnnormalisedVotingPower,
getOverstakingPenalty,
getOverstakedAmount,
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
@@ -21,10 +22,9 @@ describe('getLastEpochScoreAndPerformance', () => {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
performanceScore: '0.75',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
},
},
},
@@ -33,10 +33,9 @@ describe('getLastEpochScoreAndPerformance', () => {
id: '0x234',
rewardScore: {
rawValidatorScore: '0.35',
performanceScore: '0.85',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.85',
},
},
},
@@ -51,14 +50,12 @@ describe('getLastEpochScoreAndPerformance', () => {
).toEqual({
rawValidatorScore: '0.25',
performanceScore: '0.75',
stakeScore: '0.25',
});
expect(
getLastEpochScoreAndPerformance(mockPreviousEpochData, '0x234')
).toEqual({
rawValidatorScore: '0.35',
performanceScore: '0.85',
stakeScore: '0.25',
});
});
});
@@ -82,34 +79,40 @@ describe('getUnnormalisedVotingPower', () => {
});
describe('getOverstakingPenalty', () => {
it('returns "0%" when both arguments are null or undefined', () => {
expect(getOverstakingPenalty(null, null)).toBe('0%');
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
it('should return the overstaking penalty', () => {
expect(
getOverstakingPenalty(new BigNumber(100), Number(1000).toString())
).toEqual('10.00%');
expect(
getOverstakingPenalty(new BigNumber(500), Number(2000).toString())
).toEqual('25.00%');
});
});
describe('getOverstakedAmount', () => {
it('should return the overstaked amount', () => {
expect(
// If a validator score is 0, any amount staked on the node is considered overstaked
getOverstakedAmount('0', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(20));
expect(
getOverstakedAmount('0.05', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(15));
expect(
getOverstakedAmount('0.1', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(10));
expect(
getOverstakedAmount('0.15', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(5));
expect(
getOverstakedAmount('0.2', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(0));
});
it('returns "0%" when one argument is null or undefined', () => {
expect(getOverstakingPenalty('10', null)).toBe('0%');
expect(getOverstakingPenalty(null, '20')).toBe('0%');
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
});
it('returns "0%" when validatorScore or stakeScore is zero', () => {
expect(getOverstakingPenalty('0', '20')).toBe('0%');
expect(getOverstakingPenalty('10', '0')).toBe('0%');
});
it('returns the correct overstaking penalty', () => {
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
});
it('handles string numbers with decimals', () => {
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
it('should return 0 if the overstaked amount is negative', () => {
expect(
getOverstakedAmount('0.8', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(0));
});
});
+19 -15
View File
@@ -15,8 +15,7 @@ export const getLastEpochScoreAndPerformance = (
return {
rawValidatorScore: validator?.rewardScore?.rawValidatorScore,
performanceScore: validator?.rewardScore?.performanceScore,
stakeScore: validator?.rankingScore?.stakeScore,
performanceScore: validator?.rankingScore?.performanceScore,
};
};
@@ -43,26 +42,31 @@ export const getPerformancePenalty = (performanceScore?: string) =>
2
);
export const getOverstakingPenalty = (
export const getOverstakedAmount = (
validatorScore: string | null | undefined,
stakeScore: string | null | undefined
totalStake: string,
stakedOnNode: string
) => {
if (!validatorScore || !stakeScore) {
return '0%';
}
const toReturn = validatorScore
? new BigNumber(stakedOnNode).minus(
new BigNumber(validatorScore).times(new BigNumber(totalStake))
)
: new BigNumber(0);
return toReturn.isNegative() ? new BigNumber(0) : toReturn;
};
export const getOverstakingPenalty = (
overstakedAmount: BigNumber,
stakedOnNode: string
) => {
// avoid division by zero
if (
new BigNumber(validatorScore).isZero() ||
new BigNumber(stakeScore).isZero()
) {
return '0%';
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
return '0';
}
return formatNumberPercentage(
new BigNumber(1)
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
.times(100),
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
2
);
};
@@ -14,7 +14,6 @@ import { TokenDetailsCirculating } from './token-details-circulating';
import { SplashLoader } from '../../../components/splash-loader';
import { useEthereumConfig } from '@vegaprotocol/web3';
import { useContracts } from '../../../contexts/contracts/contracts-context';
import { ENV } from '../../../config';
export const TokenDetails = ({
totalSupply,
@@ -50,9 +49,6 @@ export const TokenDetails = ({
);
}
const tokenVestingContractAddress =
config.token_vesting_contract?.address || ENV.addresses.tokenVestingAddress;
return (
<div className="token-details">
<RoundedWrapper>
@@ -69,20 +65,18 @@ export const TokenDetails = ({
{token.address}
</Link>
</KeyValueTableRow>
{tokenVestingContractAddress && (
<KeyValueTableRow>
{t('Vesting contract').toUpperCase()}
<Link
data-testid="token-contract"
title={t('View on Etherscan (opens in a new tab)')}
className="font-mono text-white text-right"
href={`${ETHERSCAN_URL}/address/${tokenVestingContractAddress}`}
target="_blank"
>
{tokenVestingContractAddress}
</Link>
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('Vesting contract').toUpperCase()}
<Link
data-testid="token-contract"
title={t('View on Etherscan (opens in a new tab)')}
className="font-mono text-white text-right"
href={`${ETHERSCAN_URL}/address/${config.token_vesting_contract.address}`}
target="_blank"
>
{config.token_vesting_contract.address}
</Link>
</KeyValueTableRow>
<KeyValueTableRow>
{t('Total supply').toUpperCase()}
<span className="font-mono" data-testid="total-supply">
+5
View File
@@ -0,0 +1,5 @@
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
NX_VEGA_ENV=SANDBOX
NX_VEGA_NETWORKS={\"DEVNET\":\"https://dev.token.vega.xyz\",\"STAGNET3\":\"https://stagnet3.token.vega.xyz\",\"STAGNET1\":\"https://stagnet1.token.vega.xyz\",\"TESTNET\":\"https://token.fairground.wtf\",\"MAINNET\":\"https://token.vega.xyz\"}
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
{
"hosts": ["https://api-n00.mainnet-mirror.vega.rocks/graphql"]
}
@@ -0,0 +1,3 @@
{
"hosts": ["https://api-n01.sandbox.vega.rocks/graphql"]
}
@@ -411,7 +411,6 @@ describe('capsule', { tags: '@slow' }, () => {
it('approved amount is less than deposit', function () {
// 1001-DEPO-006
// 1001-DEPO-007
cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click();
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
@@ -431,8 +430,8 @@ describe('capsule', { tags: '@slow' }, () => {
});
it('withdraw - delay verification', function () {
// 1001-DEPO-007
// 1001-DEPO-024
// 1002-WITH-007
cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist');
@@ -76,7 +76,6 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('insufficient funds', () => {
// 1001-DEPO-004
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
@@ -88,10 +87,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
.should('have.text', 'Insufficient amount in Ethereum wallet');
});
});
+1 -81
View File
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
@@ -85,7 +85,6 @@ describe('markets table', { tags: '@smoke' }, () => {
cy.getByTestId('view-market-list-link')
.should('have.attr', 'href', '#/markets/all')
.click();
cy.get('[data-testid="All markets"]').should(
'have.attr',
'data-state',
@@ -118,85 +117,6 @@ describe('markets table', { tags: '@smoke' }, () => {
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
);
});
it('proposed markets tab should be sorted properly', () => {
cy.getByTestId('view-market-list-link').click();
cy.get('[data-testid="Proposed markets"]').click();
const marketColDefault = [
'ETHUSD',
'LINKUSD',
'ETHUSD',
'ETHDAI.MF21',
'AAPL.MF21',
'BTCUSD.MF21',
'TSLA.QM21',
'AAVEDAI.MF21',
'ETHBTC.QM21',
'UNIDAI.MF21',
];
const marketColAsc = [
'AAPL.MF21',
'AAVEDAI.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'ETHDAI.MF21',
'ETHUSD',
'ETHUSD',
'LINKUSD',
'TSLA.QM21',
'UNIDAI.MF21',
];
const marketColDesc = [
'UNIDAI.MF21',
'TSLA.QM21',
'LINKUSD',
'ETHUSD',
'ETHUSD',
'ETHDAI.MF21',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting('market', marketColDefault, marketColAsc, marketColDesc);
const stateColDefault = [
'Open',
'Passed',
'Waiting for Node Vote',
'Open',
'Passed',
'Open',
'Passed',
'Open',
'Waiting for Node Vote',
'Open',
];
const stateColAsc = [
'Open',
'Open',
'Open',
'Open',
'Open',
'Passed',
'Passed',
'Passed',
'Waiting for Node Vote',
'Waiting for Node Vote',
];
const stateColDesc = [
'Waiting for Node Vote',
'Waiting for Node Vote',
'Passed',
'Passed',
'Passed',
'Open',
'Open',
'Open',
'Open',
'Open',
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('opening auction subsets should be properly displayed', () => {
cy.mockTradingPage(
@@ -1,329 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import { mockConnectWallet } from '@vegaprotocol/cypress';
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return {
code: Schema.OrderTimeInForceCode[value],
value,
text: Schema.OrderTimeInForceMapping[value],
};
});
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-030
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('post and reduce order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
validTIF.forEach((tif) => {
it(`post and reduce order limit for ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
const validTIFLimit = TIFlist.filter((tif) =>
['GFA', 'GFN', 'GTC', 'GTT'].includes(tif.code)
);
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
});
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { aliasGQLQuery, mockConnectWallet } from '@vegaprotocol/cypress';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import {
@@ -13,6 +13,8 @@ const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
@@ -30,6 +32,34 @@ const displayTomorrow = () => {
return tomorrow.toISOString().substring(0, 16);
};
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-031
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
@@ -363,6 +393,227 @@ describe(
}
);
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('suspended market validation', { tags: '@regression' }, () => {
before(() => {
cy.setVegaWallet();
@@ -514,8 +765,6 @@ describe('account validation', { tags: '@regression' }, () => {
it('must show error returned by wallet ', () => {
// 0003-WTXN-009
// 0003-WTXN-011
// 0002-WCON-016
// 0003-WTXN-008
//trigger error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
@@ -539,8 +788,6 @@ describe('account validation', { tags: '@regression' }, () => {
'contain.text',
'The connection to your Vega Wallet has been lost.'
);
cy.getByTestId('connect-vega-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
});
it('must see that the order was rejected by the connected wallet', () => {
@@ -117,7 +117,6 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-009
mockConnectWallet();
cy.getByTestId(connectVegaBtn).click();
@@ -125,10 +124,6 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(dialogContent).should(
'contain.text',
'Approve the connection from your Vega wallet app.'
);
cy.getByTestId(dialogContent).should('not.exist');
cy.getByTestId(manageVegaBtn).should('exist');
});
@@ -137,7 +132,6 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-015
mockConnectWalletWithUserError();
cy.getByTestId(connectVegaBtn).click();
@@ -8,7 +8,6 @@ import {
import { tooltipMapping } from '@vegaprotocol/market-info';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -24,7 +23,6 @@ import {
Tabs,
Link as UiToolkitLink,
Indicator,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
@@ -39,7 +37,6 @@ import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import { useEnvironment } from '@vegaprotocol/environment';
const enum LiquidityTabs {
Active = 'active',
@@ -147,7 +144,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
const { VEGA_DOCS_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -215,13 +211,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat heading={t('Market ID')}>
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
{VEGA_DOCS_URL && (
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
{t('Providing liquidity')}
</ExternalLink>
)}
</HeaderStat>
</Header>
);
});
@@ -301,7 +301,7 @@ export const AccountHistoryChart = ({
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: [string, string]; rows: [Date, number][] } | null =
const values: { cols: string[]; rows: [Date, ...number[]][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
@@ -8,7 +8,6 @@ import type { MarketData } from '@vegaprotocol/market-list';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import {
ExternalLink,
Indicator,
KeyValueTable,
KeyValueTableRow,
@@ -19,11 +18,9 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
interface Props {
marketId?: string;
@@ -47,8 +44,6 @@ export const MarketLiquiditySupplied = ({
params.market_liquidity_targetstake_triggering_ratio
);
const { VEGA_DOCS_URL } = useEnvironment();
const variables = useMemo(
() => ({
marketId: marketId || '',
@@ -131,14 +126,6 @@ export const MarketLiquiditySupplied = ({
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
className="mt-2"
>
{t('Learn about providing liquidity')}
</ExternalLink>
)}
{showMessage && (
<p className="mt-4">
{t(
+1 -5
View File
@@ -23,7 +23,6 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { createDocsLinks } from '@vegaprotocol/utils';
export const Navbar = ({
theme = 'system',
@@ -38,7 +37,6 @@ export const Navbar = ({
const tradingPath = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKET]();
return (
<Navigation
appName="Console"
@@ -91,9 +89,7 @@ export const Navbar = ({
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavExternalLink
href={createDocsLinks(VEGA_DOCS_URL).NEW_TO_VEGA}
>
<NavExternalLink href={VEGA_DOCS_URL}>
{t('Docs')}
</NavExternalLink>
</NavigationItem>
@@ -185,7 +185,7 @@ export const columns = (
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey || e.ctrlKey);
onSelect(market.id, e.metaKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
@@ -366,7 +366,7 @@ export const columnsPositionMarkets = (
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey || e.ctrlKey);
onSelect(market.id, e.metaKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
@@ -44,7 +44,7 @@ export const SelectMarketTableRow = ({
<tr
className={`hover:bg-neutral-200 dark:hover:bg-neutral-700 cursor-pointer relative h-[34px]`}
onClick={(ev) => {
onSelect(marketId, ev.metaKey || ev.ctrlKey);
onSelect(marketId, ev.metaKey);
}}
data-testid={`market-link-${marketId}`}
>
@@ -8,10 +8,6 @@ import type {
MarketData,
} from '@vegaprotocol/market-list';
import { SelectMarketLandingTable } from './welcome-landing-dialog';
const mockMarketClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: () => mockMarketClickHandler,
}));
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
type PartialMarket = Partial<
@@ -178,25 +174,4 @@ describe('WelcomeLandingDialog', () => {
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
expect(onClose).toHaveBeenCalled();
});
it('should not call onClose when metaKey is held', () => {
const onClose = jest.fn();
render(
<MemoryRouter>
<SelectMarketLandingTable
markets={[MARKET_A as Market, MARKET_B as Market]}
onClose={onClose}
/>
</MemoryRouter>,
{ wrapper: MockedProvider }
);
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0], {
metaKey: true,
});
expect(mockMarketClickHandler).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onClose).toHaveBeenCalled();
});
});
@@ -12,10 +12,9 @@ import {
SelectMarketTableRow,
} from '../select-market';
import { WelcomeDialogHeader } from './welcome-dialog-header';
import { Link } from 'react-router-dom';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { ProposedMarkets } from './proposed-markets';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const SelectMarketLandingTable = ({
markets,
@@ -24,14 +23,24 @@ export const SelectMarketLandingTable = ({
markets: MarketMaybeWithDataAndCandles[] | null;
onClose: () => void;
}) => {
const onSelect = useMarketClickHandler();
const onSelectMarket = useCallback(
(id: string, metaKey?: boolean) => {
onSelect(id, metaKey);
if (!metaKey) {
onClose();
const params = useParams();
const navigate = useNavigate();
const marketId = params.marketId;
const onSelect = useCallback(
(id: string) => {
if (id && id !== marketId) {
navigate(Links[Routes.MARKET](id));
}
},
[marketId, navigate]
);
const onSelectMarket = useCallback(
(id: string) => {
onSelect(id);
onClose();
},
[onSelect, onClose]
);
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
@@ -64,7 +73,7 @@ export const SelectMarketLandingTable = ({
key={i}
detailed={false}
onSelect={onSelectMarket}
columns={columns(market, onSelectMarket, onCellClick)}
columns={columns(market, onSelect, onCellClick)}
/>
))}
</tbody>
@@ -7,11 +7,7 @@ import {
} from '@vegaprotocol/types';
import type { VegaStoredTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import {
VegaTransactionDetails,
getVegaTransactionContentIntent,
} from './use-vega-transaction-toasts';
import { Intent } from '@vegaprotocol/ui-toolkit';
import { VegaTransactionDetails } from './use-vega-transaction-toasts';
jest.mock('@vegaprotocol/assets', () => {
const A1 = {
@@ -282,27 +278,3 @@ describe('VegaTransactionDetails', () => {
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
});
});
describe('getVegaTransactionContentIntent', () => {
it('returns the correct intent for a transaction', () => {
expect(getVegaTransactionContentIntent(withdraw).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
Intent.Success
);
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
Intent.Success
);
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(closePosition).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(batch).intent).toBe(Intent.Primary);
});
});
@@ -39,14 +39,10 @@ import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import {
getOrderToastIntent,
getOrderToastTitle,
getRejectionReason,
useOrderByIdQuery,
} from '@vegaprotocol/orders';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Side } from '@vegaprotocol/types';
import { OrderStatus } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
import { Size } from '@vegaprotocol/react-helpers';
@@ -482,10 +478,12 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
return (
<>
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
{rejectionReason ? (
<ToastHeading>{t('Order rejected')}</ToastHeading>
{rejectionReason || tx.order.rejectionReason ? (
<p>
{t('Your order has been rejected because: %s', [rejectionReason])}
{t('Your order has been rejected because: %s', [
rejectionReason || tx.order.rejectionReason,
])}
</p>
) : (
<p>{t('Your order has been rejected.')}</p>
@@ -508,7 +506,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
return (
<div>
<h3 className="font-bold">{getOrderToastTitle(tx.order.status)}</h3>
<h3 className="font-bold">{t('Order rejected')}</h3>
<p>{t('Your order was rejected.')}</p>
{tx.txHash && (
<p className="break-all">
@@ -547,11 +545,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
return (
<>
<ToastHeading>
{tx.order?.status
? getOrderToastTitle(tx.order.status)
: t('Confirmed')}
</ToastHeading>
<ToastHeading>{t('Confirmed')}</ToastHeading>
<p>{t('Your transaction has been confirmed ')}</p>
{tx.txHash && (
<p className="break-all">
@@ -586,7 +580,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
tx.error instanceof WalletError &&
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
label = t('Order rejected');
errorMessage = t('Your order has been rejected because: %s', [
orderRejection || tx.order?.rejectionReason || ' ',
]);
@@ -638,8 +632,30 @@ export const useVegaTransactionToasts = () => {
);
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
let content: ToastContent;
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
const { intent, content } = getVegaTransactionContentIntent(tx);
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order &&
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
tx.order.status
)) ||
tx.order?.rejectionReason
? Intent.Danger
: intentMap[tx.status];
return {
id: `vega-${tx.id}`,
@@ -663,24 +679,3 @@ export const useVegaTransactionToasts = () => {
}
);
};
export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
let content: ToastContent;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order && getOrderToastIntent(tx.order.status)) || intentMap[tx.status];
return { intent, content };
};
-15
View File
@@ -34,18 +34,6 @@ html [data-theme='dark'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.500');
/* studies */
--pennant-color-eldar-ray-bear-power: theme('colors.vega.pink.500');
--pennant-color-eldar-ray-bull-power: theme('colors.vega.green.650');
--pennant-color-macd-divergence-buy: theme('colors.vega.green.650');
--pennant-color-macd-divergence-sell: theme('colors.vega.pink.500');
--pennant-color-macd-signal: theme('colors.vega.blue.500');
--pennant-color-macd-macd: theme('colors.vega.yellow.500');
--pennant-color-volume-buy: theme('colors.vega.green.650');
--pennant-color-volume-sell: theme('colors.vega.pink.500');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.650');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.500');
@@ -62,9 +50,6 @@ html [data-theme='light'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.400');
--pennant-color-volume-buy: theme('colors.vega.green.400');
--pennant-color-volume-sell: theme('colors.vega.pink.400');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.400');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.550');
+72 -221
View File
@@ -1,10 +1,4 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
@@ -33,34 +27,6 @@ describe('TransferForm', () => {
submitTransfer: jest.fn(),
};
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
});
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
});
});
it('validates fields and submits', async () => {
render(<TransferForm {...props} />);
@@ -96,17 +62,15 @@ describe('TransferForm', () => {
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
// Test amount validation
fireEvent.change(amountInput, {
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.00000001' },
});
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
fireEvent.change(amountInput, {
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '9999999' },
});
expect(
@@ -114,7 +78,7 @@ describe('TransferForm', () => {
).toBeInTheDocument();
// set valid amount
fireEvent.change(amountInput, {
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amount },
});
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
@@ -136,191 +100,78 @@ describe('TransferForm', () => {
});
});
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
// check include fees checkbox
fireEvent.click(checkbox);
});
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
submit();
await waitFor(() => {
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
oneOff: {},
});
});
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
});
it('validates fields when checkbox is not checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
});
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
});
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
fee: '0.2',
transferAmount: '200',
decimals: 8,
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(props.amount).plus(expected).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
props.amount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
});
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
});
});
+16 -102
View File
@@ -16,7 +16,6 @@ import {
RichSelect,
Select,
Tooltip,
Checkbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -64,26 +63,6 @@ export const TransferForm = ({
const amount = watch('amount');
const assetId = watch('asset');
const [includeFee, setIncludeFee] = useState(false);
const transferAmount = useMemo(() => {
if (!amount) return undefined;
if (includeFee && feeFactor) {
return new BigNumber(1).minus(feeFactor).times(amount).toString();
}
return amount;
}, [amount, includeFee, feeFactor]);
const fee = useMemo(() => {
if (!transferAmount) return undefined;
if (includeFee) {
return new BigNumber(amount).minus(transferAmount).toString();
}
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
@@ -93,16 +72,13 @@ export const TransferForm = ({
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
id: asset.id,
decimals: asset.decimals,
});
submitTransfer(transfer);
},
[asset, submitTransfer, transferAmount]
[asset, submitTransfer]
);
const min = useMemo(() => {
@@ -237,32 +213,7 @@ export const TransferForm = ({
<InputError forInput="amount">{errors.amount.message}</InputError>
)}
</FormGroup>
<div className="mb-4">
<Checkbox
name="include-transfer-fee"
disabled={!transferAmount}
label={
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>{t('Include transfer fee')}</div>
</Tooltip>
}
checked={includeFee}
onCheckedChange={() => setIncludeFee(!includeFee)}
/>
</div>
{transferAmount && fee && (
<TransferFee
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fee}
decimals={asset?.decimals}
/>
)}
<TransferFee amount={amount} feeFactor={feeFactor} />
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</Button>
@@ -272,71 +223,34 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
transferAmount,
feeFactor,
fee,
decimals,
}: {
amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string;
decimals?: number;
}) => {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (!feeFactor || !amount) return null;
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
// using toFixed without an argument will always return a
// number in normal notation without rounding, formatting functions
// arent working in a way which won't round the decimal places
const value = new BigNumber(amount).times(feeFactor).toFixed();
return (
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
<div>
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
[feeFactor]
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(fee, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
)}
>
<div>{t('Amount to be transferred')}</div>
</Tooltip>
<div
data-testid="transfer-amount"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
)}
>
<div>{t('Total amount (with fee)')}</div>
</Tooltip>
<div
data-testid="total-transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(totalValue, decimals)}
</div>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{value}
</div>
</div>
);
-5
View File
@@ -1,6 +1 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { defaultFallbackInView } from 'react-intersection-observer';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;
+2 -2
View File
@@ -1,6 +1,6 @@
import 'pennant/dist/style.css';
import {
CandlestickChart,
Chart,
ChartType,
Interval,
Overlay,
@@ -234,7 +234,7 @@ export const CandlesChartContainer = ({
</DropdownMenu>
</div>
<div className="flex-1">
<CandlestickChart
<Chart
dataSource={dataSource}
options={{
chartType: chartType,
-3
View File
@@ -52,9 +52,6 @@ export const checkSorting = (
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { BID_COLOR, ASK_COLOR } from './vol-cell';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { NumericCell } from './numeric-cell';
export interface CumulativeVolProps {
@@ -55,7 +55,7 @@ export const CumulativeVol = memo(
(
<NumericCell
value={Number(indicativeVolume)}
valueFormatted={addDecimalsFixedFormatNumber(
valueFormatted={addDecimalsFormatNumber(
indicativeVolume,
positionDecimalPlaces ?? 0
)}
@@ -67,7 +67,7 @@ export const CumulativeVol = memo(
{ask ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFixedFormatNumber(
valueFormatted={addDecimalsFormatNumber(
ask,
positionDecimalPlaces ?? 0
)}
@@ -77,7 +77,7 @@ export const CumulativeVol = memo(
{bid ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFixedFormatNumber(
valueFormatted={addDecimalsFormatNumber(
bid,
positionDecimalPlaces ?? 0
)}
@@ -21,7 +21,7 @@ export const MarketNameCell = ({
ev.preventDefault();
ev.stopPropagation();
if (onMarketClick) {
onMarketClick(id, ev.metaKey || ev.ctrlKey);
onMarketClick(id, ev.metaKey);
}
},
[id, onMarketClick]
@@ -78,9 +78,7 @@ export const compileGridData = (
label: (
<Link
to={`/liquidity/${market.id}`}
onClick={(ev) =>
onSelect && onSelect(market.id, ev.metaKey || ev.ctrlKey)
}
onClick={(ev) => onSelect && onSelect(market.id, ev.metaKey)}
>
<UILink>{t('Current liquidity')}</UILink>
</Link>
+4
View File
@@ -1,2 +1,6 @@
export * from './__generated__/EstimateOrder';
export * from './use-calculate-slippage';
export * from './use-fee-deal-ticket-details';
export * from './use-market-positions';
export * from './use-maximum-position-size';
export * from './use-order-closeout';
@@ -0,0 +1,144 @@
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/market-list';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useCalculateSlippage } from './use-calculate-slippage';
const mockData = {
decimalPlaces: 0,
positionDecimalPlaces: 0,
depth: {
buy: [
{
price: '5',
volume: '2',
},
{
price: '4',
volume: '3',
},
{
price: '3',
volume: '2',
},
{
price: '2',
volume: '1',
},
{
price: '1',
volume: '1',
},
],
sell: [
{
price: '6',
volume: '1',
},
{
price: '7',
volume: '3',
},
{
price: '8',
volume: '2',
},
{
price: '9',
volume: '1',
},
{
price: '10',
volume: '2',
},
],
},
};
let mockOrderBookData = {
data: mockData,
};
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn(() => ({
data: {
marketsConnection: [],
},
})),
useThrottledDataProvider: jest.fn(() => mockOrderBookData),
}));
describe('useCalculateSlippage Hook', () => {
describe('calculate proper result', () => {
afterEach(() => {
jest.clearAllMocks();
});
const market = {
id: 'marketId',
decimalPlaces: 0,
positionDecimalPlaces: 0,
} as Market;
it('long order', () => {
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_BUY,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('33.33');
});
it('short order', () => {
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_SELL,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('31.11');
});
it('when no order book result should be null', () => {
mockOrderBookData = {
data: {
...mockData,
depth: {
...mockData.depth,
buy: [],
},
},
};
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_SELL,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toBeNull();
});
});
});
@@ -0,0 +1,63 @@
import { marketDepthProvider } from '@vegaprotocol/market-depth';
import * as Schema from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/market-list';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { BigNumber } from 'bignumber.js';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { useThrottledDataProvider } from '@vegaprotocol/react-helpers';
interface Props {
market: Market;
order: OrderSubmissionBody['orderSubmission'];
}
export const useCalculateSlippage = ({ market, order }: Props) => {
const { data } = useThrottledDataProvider(
{
dataProvider: marketDepthProvider,
variables: { marketId: market.id },
},
1000
);
const volPriceArr =
data?.depth[order.side === Schema.Side.SIDE_BUY ? 'sell' : 'buy'] || [];
if (volPriceArr.length && market) {
const decimals = market.decimalPlaces ?? 0;
const positionDecimals = market.positionDecimalPlaces ?? 0;
const bestPrice = toBigNum(volPriceArr[0].price, decimals);
const { size } = order;
let descSize = new BigNumber(size);
let i = 0;
const volPricePairs: Array<[BigNumber, BigNumber]> = [];
while (!descSize.isZero() && i < volPriceArr.length) {
const price = toBigNum(volPriceArr[i].price, decimals);
const amount = BigNumber.min(
descSize,
toBigNum(volPriceArr[i].volume, positionDecimals)
);
volPricePairs.push([price, amount]);
descSize = BigNumber.max(0, descSize.minus(amount));
i++;
}
if (volPricePairs.length) {
const volWeightAvPricePair = volPricePairs.reduce(
(agg, item) => {
agg[0] = agg[0].plus(item[0].multipliedBy(item[1]));
agg[1] = agg[1].plus(item[1]);
return agg;
},
[new BigNumber(0), new BigNumber(0)]
);
const volWeightAvPrice = volWeightAvPricePair[0].dividedBy(
volWeightAvPricePair[1]
);
const slippage = volWeightAvPrice
.minus(bestPrice)
.absoluteValue()
.dividedBy(bestPrice)
.multipliedBy(100);
return formatNumber(slippage, 2);
}
}
return null;
};
@@ -18,6 +18,7 @@ import {
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
} from '../constants';
import { useOrderCloseOut } from './use-order-closeout';
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
import { getDerivedPrice } from '../utils/get-price';
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
@@ -48,6 +49,12 @@ export const useFeeDealTicketDetails = (
skip: !pubKey || !market || !order.size || !price,
});
const estCloseOut = useOrderCloseOut({
order,
market,
marketData,
});
const notionalSize = useMemo(() => {
if (price && order.size) {
return toBigNum(order.size, market.positionDecimalPlaces)
@@ -67,8 +74,16 @@ export const useFeeDealTicketDetails = (
notionalSize,
accountBalance,
estimateOrder: estMargin?.estimateOrder,
estCloseOut,
};
}, [market, assetSymbol, notionalSize, accountBalance, estMargin]);
}, [
market,
assetSymbol,
notionalSize,
accountBalance,
estMargin,
estCloseOut,
]);
};
export interface FeeDetails {
@@ -77,6 +92,7 @@ export interface FeeDetails {
market: Market;
assetSymbol: string;
notionalSize: string | null;
estCloseOut: string | null;
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
estimatedInitialMargin: string;
estimatedTotalInitialMargin: string;
@@ -0,0 +1,53 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { useMarketPositions } from './use-market-positions';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockMarketAccountBalance: {
accountBalance: string;
accountDecimals: number | null;
} = { accountBalance: '50001000000', accountDecimals: 5 };
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useMarketAccountBalance: jest.fn(() => mockMarketAccountBalance),
}));
jest.mock('@vegaprotocol/positions', () => ({
...jest.requireActual('@vegaprotocol/positions'),
useMarketPositionOpenVolume: jest.fn(() => '100002'),
}));
describe('useOrderPosition Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper positive value', () => {
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current?.openVolume).toEqual('100002');
expect(result.current?.balance).toEqual('50001000000');
});
it('if balance equal 0 return null', () => {
mockMarketAccountBalance = { accountBalance: '0', accountDecimals: 5 };
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current).toBeNull();
});
it('if no markets return null', () => {
mockMarketAccountBalance = { accountBalance: '', accountDecimals: null };
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current).toBeNull();
});
});
@@ -0,0 +1,34 @@
import { useMemo } from 'react';
import { BigNumber } from 'bignumber.js';
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
import { useMarketPositionOpenVolume } from '@vegaprotocol/positions';
interface Props {
marketId: string;
}
export type PositionMargin = {
openVolume: string;
balance: string;
balanceDecimals?: number;
} | null;
export const useMarketPositions = ({ marketId }: Props): PositionMargin => {
const { accountBalance, accountDecimals } = useMarketAccountBalance(marketId);
const openVolume = useMarketPositionOpenVolume(marketId);
return useMemo(() => {
if (accountBalance && accountDecimals) {
const balance = new BigNumber(accountBalance);
const volume = new BigNumber(openVolume);
if (!balance.isZero() && !volume.isZero()) {
return {
balance: accountBalance,
balanceDecimals: accountDecimals,
openVolume,
};
}
}
return null;
}, [accountBalance, accountDecimals, openVolume]);
};
@@ -0,0 +1,120 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { PositionMargin } from './use-market-positions';
import { useMaximumPositionSize } from './use-maximum-position-size';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockAccountBalance: {
accountBalance: string;
accountDecimals: number | null;
} = { accountBalance: '200000', accountDecimals: 5 };
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useAccountBalance: jest.fn(() => mockAccountBalance),
}));
const defaultMockMarketPositions = {
openVolume: '1',
balance: '100000',
};
let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions;
const mockOrder: OrderSubmissionBody['orderSubmission'] = {
type: Schema.OrderType.TYPE_MARKET,
size: '1',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
marketId: 'market-id',
};
jest.mock('./use-market-positions', () => ({
useMarketPositions: ({
marketId,
partyId,
}: {
marketId: string;
partyId: string;
}) => mockMarketPositions,
}));
describe('useMaximumPositionSize', () => {
it('should return correct size when no open positions', () => {
mockMarketPositions = null;
const price = '50';
const expected = 4000;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and same side', () => {
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 3999;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and opposite side', () => {
const price = '50';
mockOrder.side = Schema.Side.SIDE_SELL;
mockMarketPositions = defaultMockMarketPositions;
const expected = 4001;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return zero if no account balance', () => {
mockAccountBalance = {
accountBalance: '0',
accountDecimals: 5,
};
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 0;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
});
@@ -0,0 +1,46 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useAccountBalance } from '@vegaprotocol/accounts';
import { BigNumber } from 'bignumber.js';
import { useMarketPositions } from './use-market-positions';
interface Props {
marketId: string;
price?: string;
settlementAssetId: string;
order: OrderSubmissionBody['orderSubmission'];
}
const getSize = (balance: string, price: string) =>
new BigNumber(balance).dividedBy(new BigNumber(price));
export const useMaximumPositionSize = ({
marketId,
price,
settlementAssetId,
order,
}: Props): number => {
const { accountBalance } = useAccountBalance(settlementAssetId) || {};
const marketPositions = useMarketPositions({ marketId: marketId });
if (!accountBalance || new BigNumber(accountBalance || 0).isZero()) {
return 0;
}
const size = getSize(accountBalance, price || '');
if (!marketPositions) {
return size.toNumber() || 0;
}
const isSameSide =
(new BigNumber(marketPositions.openVolume).isPositive() &&
order.side === Schema.Side.SIDE_BUY) ||
(new BigNumber(marketPositions.openVolume).isNegative() &&
order.side === Schema.Side.SIDE_SELL);
const adjustedForVolume = new BigNumber(size)[isSameSide ? 'minus' : 'plus'](
marketPositions.openVolume
);
return adjustedForVolume.isNegative() ? 0 : adjustedForVolume.toNumber();
};
@@ -0,0 +1,117 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { Market, MarketData } from '@vegaprotocol/market-list';
import { useOrderCloseOut } from './use-order-closeout';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockMarketMargin: string | undefined = undefined;
jest.mock('@vegaprotocol/positions', () => ({
...jest.requireActual('@vegaprotocol/positions'),
useMarketMargin: () => mockMarketMargin,
}));
describe('useOrderCloseOut', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
} as unknown as Market;
const marketData = {
markPrice: 100000,
} as unknown as MarketData;
beforeEach(() => {
jest.clearAllMocks();
});
it('should return proper null value', () => {
mockMarketMargin = '-1';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: order as OrderSubmissionBody['orderSubmission'],
market,
marketData: {
markPrice: '0',
} as MarketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual(null);
});
it('should return proper sell value', () => {
mockMarketMargin = '0';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('1');
});
it('should return proper sell value on limit order', () => {
mockMarketMargin = '0';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
price: '1000000',
type: 'TYPE_LIMIT',
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('1000000');
});
it('should return proper empty value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData: {
markPrice: '0',
} as MarketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('0');
});
});
@@ -0,0 +1,61 @@
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { Market, MarketData } from '@vegaprotocol/market-list';
import {
useAccountBalance,
useMarketAccountBalance,
} from '@vegaprotocol/accounts';
import { useMarketMargin } from '@vegaprotocol/positions';
import { useMarketPositions } from './use-market-positions';
interface Props {
order: OrderSubmissionBody['orderSubmission'];
market: Market;
marketData: MarketData;
}
export const useOrderCloseOut = ({
order,
market,
marketData,
}: Props): string | null => {
const { accountBalance, accountDecimals } = useAccountBalance(
market.tradableInstrument.instrument.product.settlementAsset.id
);
const { accountBalance: positionBalance, accountDecimals: positionDecimals } =
useMarketAccountBalance(market.id);
const maintenanceLevel = useMarketMargin(market.id);
const marginMaintenanceLevel = new BigNumber(
addDecimal(maintenanceLevel || 0, market.decimalPlaces)
);
const positionAccountBalance = new BigNumber(
addDecimal(positionBalance || 0, positionDecimals || 0)
);
const generalAccountBalance = new BigNumber(
addDecimal(accountBalance || 0, accountDecimals || 0)
);
const { openVolume } =
useMarketPositions({
marketId: market.id,
}) || {};
const volume = new BigNumber(
addDecimal(openVolume || '0', market.positionDecimalPlaces)
)[order.side === Schema.Side.SIDE_BUY ? 'plus' : 'minus'](order.size);
const price =
order.type === Schema.OrderType.TYPE_LIMIT && order.price
? new BigNumber(order.price)
: new BigNumber(addDecimal(marketData.markPrice, market.decimalPlaces));
// regarding formula (marginMaintenanceLevel - positionAccountBalance - generalAccountBalance) / volume + markPrice
const marginDifference = marginMaintenanceLevel
.minus(positionAccountBalance)
.minus(generalAccountBalance);
const closeOut = marginDifference.div(volume).plus(price);
if (closeOut.isPositive()) {
return closeOut.toString();
}
return null;
};
+15 -14
View File
@@ -51,12 +51,11 @@ export const ApproveNotification = ({
intent={intent}
testId="approve-default"
message={t(
'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
selectedAsset?.symbol
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
)}
buttonProps={{
size: 'sm',
text: t('Approve %s', selectedAsset?.symbol),
text: `Approve ${selectedAsset?.symbol}`,
action: onApprove,
dataTestId: 'approve-submit',
}}
@@ -69,12 +68,13 @@ export const ApproveNotification = ({
intent={intent}
testId="reapprove-default"
message={t(
'Approve again to deposit more than %s',
formatNumber(balances.allowance.toString())
`Approve again to deposit more than ${formatNumber(
balances.allowance.toString()
)}`
)}
buttonProps={{
size: 'sm',
text: t('Approve %s', selectedAsset?.symbol),
text: `Approve ${selectedAsset?.symbol}`,
action: onApprove,
dataTestId: 'reapprove-submit',
}}
@@ -157,8 +157,7 @@ const ApprovalTxFeedback = ({
intent={Intent.Warning}
testId="approve-requested"
message={t(
'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
selectedAsset?.symbol
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
)}
/>
</div>
@@ -175,8 +174,7 @@ const ApprovalTxFeedback = ({
<>
<p>
{t(
'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
selectedAsset?.symbol
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
)}{' '}
</p>
{txLink && <p>{txLink}</p>}
@@ -196,10 +194,13 @@ const ApprovalTxFeedback = ({
message={
<>
<p>
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
])}
{t(
`You can now make deposits in ${
selectedAsset?.symbol
}, up to a maximum of ${formatNumber(
allowance?.toString() || 0
)}`
)}
</p>
{txLink && <p>{txLink}</p>}
</>
+7 -47
View File
@@ -1,11 +1,4 @@
import {
waitFor,
fireEvent,
render,
screen,
act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form';
@@ -68,7 +61,6 @@ beforeEach(() => {
submitDeposit: jest.fn(),
submitFaucet: jest.fn(),
onDisconnect: jest.fn(),
handleAmountChange: jest.fn(),
approveTxId: null,
faucetTxId: null,
isFaucetable: true,
@@ -148,14 +140,12 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5"
)
await screen.findByText('Insufficient amount in Ethereum wallet')
).toBeInTheDocument();
});
it('fails when submitted amount is more than the maximum limit', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
render(<DepositForm {...props} />);
const amountMoreThanLimit = '21';
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -164,9 +154,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText(
"You can't deposit more than your remaining deposit allowance, 10 asset-symbol"
)
await screen.findByText('Amount is above lifetime deposit limit')
).toBeInTheDocument();
});
@@ -190,9 +178,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText(
"You can't deposit more than your approved deposit amount, 30"
)
await screen.findByText('Amount is above approved amount')
).toBeInTheDocument();
});
@@ -296,6 +282,8 @@ describe('Deposit form', () => {
expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
'50'
);
expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -376,32 +364,4 @@ describe('Deposit form', () => {
/this app only works on/i
);
});
it('Remaining deposit allowance tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
});
});
it('Ethereum deposit cap tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
});
});
});
+60 -81
View File
@@ -8,7 +8,6 @@ import {
maxSafe,
addDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
@@ -26,9 +25,11 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ChangeEvent, ReactNode } from 'react';
import { useMemo, useState } from 'react';
import { useWatch, Controller, useForm } from 'react-hook-form';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import { useWatch } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
@@ -39,7 +40,6 @@ import {
import type { DepositBalances } from './use-deposit-balances';
import { FaucetNotification } from './faucet-notification';
import { ApproveNotification } from './approve-notification';
import { usePersistentDeposit } from './use-persistent-deposit';
interface FormFields {
asset: string;
@@ -53,7 +53,6 @@ export interface DepositFormProps {
selectedAsset?: Asset;
balances: DepositBalances | null;
onSelectAsset: (assetId: string) => void;
handleAmountChange: (amount: string) => void;
onDisconnect: () => void;
submitApprove: () => void;
approveTxId: number | null;
@@ -72,7 +71,6 @@ export const DepositForm = ({
selectedAsset,
balances,
onSelectAsset,
handleAmountChange,
onDisconnect,
submitApprove,
submitDeposit,
@@ -87,8 +85,6 @@ export const DepositForm = ({
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
const [approveNotificationIntent, setApproveNotificationIntent] =
useState<Intent>(Intent.Warning);
const [persistedDeposit] = usePersistentDeposit(selectedAsset?.id);
const {
register,
handleSubmit,
@@ -99,8 +95,7 @@ export const DepositForm = ({
} = useForm<FormFields>({
defaultValues: {
to: pubKey ? pubKey : undefined,
asset: selectedAsset?.id,
amount: persistedDeposit?.amount,
asset: selectedAsset?.id || '',
},
});
@@ -134,8 +129,11 @@ export const DepositForm = ({
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
}, [_pubKeys]);
const approved =
balances && balances.allowance.isGreaterThan(0) ? true : false;
const approved = balances
? balances.allowance.isGreaterThan(0)
? true
: false
: false;
return (
<form
@@ -192,44 +190,6 @@ export const DepositForm = ({
<InputError intent="danger">{errors.from.message}</InputError>
)}
</FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
@@ -286,7 +246,45 @@ export const DepositForm = ({
selectedAsset={selectedAsset}
faucetTxId={faucetTxId}
/>
{approved && selectedAsset && balances && (
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
{selectedAsset && balances && (
<div className="mb-6">
<DepositLimits {...balances} asset={selectedAsset} />
</div>
@@ -303,15 +301,8 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => {
const value = new BigNumber(v);
const allowance = new BigNumber(balances?.allowance || 0);
if (value.isGreaterThan(allowance)) {
return t(
"You can't deposit more than your approved deposit amount, %s %s",
[
formatNumber(allowance.toString()),
selectedAsset?.symbol || ' ',
]
);
if (value.isGreaterThan(balances?.allowance || 0)) {
return t('Amount is above approved amount');
}
return true;
},
@@ -327,24 +318,14 @@ export const DepositForm = ({
}
if (value.isGreaterThan(lifetimeLimit)) {
return t(
"You can't deposit more than your remaining deposit allowance, %s %s",
[
formatNumber(lifetimeLimit.toString()),
selectedAsset?.symbol || ' ',
]
);
return t('Amount is above lifetime deposit limit');
}
return true;
},
balance: (v) => {
const value = new BigNumber(v);
const balance = new BigNumber(balances?.balance || 0);
if (value.isGreaterThan(balance)) {
return t(
"You can't deposit more than you have in your Ethereum wallet, %s %s",
[formatNumber(balance), selectedAsset?.symbol || ' ']
);
if (value.isGreaterThan(balances?.balance || 0)) {
return t('Insufficient amount in Ethereum wallet');
}
return true;
},
@@ -352,9 +333,6 @@ export const DepositForm = ({
return maxSafe(balances?.balance || new BigNumber(0))(v);
},
},
onChange: (e: ChangeEvent<HTMLInputElement>) => {
handleAmountChange(e.target.value || '');
},
})}
/>
{errors.amount?.message && (
@@ -365,9 +343,10 @@ export const DepositForm = ({
{selectedAsset && balances && (
<UseButton
onClick={() => {
const amount = balances.balance.toFixed(selectedAsset.decimals);
setValue('amount', amount);
handleAmountChange(amount);
setValue(
'amount',
balances.balance.toFixed(selectedAsset.decimals)
);
clearErrors('amount');
}}
>
@@ -420,7 +399,7 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill
fill={true}
disabled={invalidChain}
>
{t('Deposit')}
+15 -47
View File
@@ -1,13 +1,8 @@
import type { Asset } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -38,35 +33,21 @@ export const DepositLimits = ({
'-'
),
},
{
key: 'MAX_LIMIT',
label: t('Lifetime deposit allowance'),
rawValue: max,
value: <CompactNumber number={max} decimals={asset.decimals} />,
},
{
key: 'DEPOSITED',
label: t('Deposited'),
rawValue: deposited,
value: <CompactNumber number={deposited} decimals={asset.decimals} />,
},
{
key: 'REMAINING',
label: (
<Tooltip
description={
<>
<p>
{t(
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
[formatNumber(max.toString()), asset.symbol]
)}
</p>
<p>
{t(
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
[
formatNumber(deposited.toString()),
asset.symbol,
formatNumber(max.minus(deposited).toString()),
asset.symbol,
]
)}
</p>
</>
}
>
<button type="button">{t('Remaining deposit allowance')}</button>
</Tooltip>
),
label: t('Remaining'),
rawValue: max.minus(deposited),
value: (
<CompactNumber
@@ -77,20 +58,7 @@ export const DepositLimits = ({
},
{
key: 'ALLOWANCE',
label: (
<Tooltip
description={
<p>
{t(
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
asset.symbol
)}
</p>
}
>
<button type="button">{t('Ethereum deposit cap')}</button>
</Tooltip>
),
label: t('Approved'),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
+2 -15
View File
@@ -5,7 +5,7 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
import sortBy from 'lodash/sortBy';
import { useSubmitApproval } from './use-submit-approval';
import { useSubmitFaucet } from './use-submit-faucet';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import { useDepositBalances } from './use-deposit-balances';
import { useDepositDialog } from './deposit-dialog';
import type { Asset } from '@vegaprotocol/assets';
@@ -14,7 +14,6 @@ import {
useBridgeContract,
useEthereumConfig,
} from '@vegaprotocol/web3';
import { usePersistentDeposit } from './use-persistent-deposit';
interface DepositManagerProps {
assetId?: string;
@@ -29,9 +28,7 @@ export const DepositManager = ({
}: DepositManagerProps) => {
const createEthTransaction = useEthTransactionStore((state) => state.create);
const { config } = useEthereumConfig();
const [persistentDeposit, savePersistentDeposit] =
usePersistentDeposit(initialAssetId);
const [assetId, setAssetId] = useState(persistentDeposit?.assetId);
const [assetId, setAssetId] = useState(initialAssetId);
const asset = assets.find((a) => a.id === assetId);
const bridgeContract = useBridgeContract();
const closeDepositDialog = useDepositDialog((state) => state.close);
@@ -68,27 +65,17 @@ export const DepositManager = ({
closeDepositDialog();
};
const onAmountChange = useCallback(
(amount: string) => {
persistentDeposit &&
savePersistentDeposit({ ...persistentDeposit, amount });
},
[savePersistentDeposit, persistentDeposit]
);
return (
<DepositForm
selectedAsset={asset}
onDisconnect={reset}
onSelectAsset={(id) => {
setAssetId(id);
savePersistentDeposit({ assetId: id });
// When we change asset, also clear the tracked faucet/approve transactions so
// we dont render stale UI
approve.reset();
faucet.reset();
}}
handleAmountChange={onAmountChange}
assets={sortBy(assets, 'name')}
submitApprove={approve.perform}
submitDeposit={submitDeposit}
@@ -1,21 +0,0 @@
import { renderHook, waitFor, act } from '@testing-library/react';
import { usePersistentDeposit } from './use-persistent-deposit';
describe('usePersistenDeposit', () => {
it('should return empty data', () => {
const { result } = renderHook(() => usePersistentDeposit());
expect(result.current).toEqual([undefined, expect.any(Function)]);
});
it('should return empty and properly saved data', async () => {
const aId = 'test';
const retObj = { assetId: 'test', amount: '1.00000' };
const { result } = renderHook(() => usePersistentDeposit(aId));
expect(result.current).toEqual([{ assetId: 'test' }, expect.any(Function)]);
await act(() => {
result.current[1](retObj);
});
await waitFor(() => {
expect(result.current[0]).toEqual(retObj);
});
});
});
@@ -1,46 +0,0 @@
import { useMemo } from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const STORAGE_KEY = 'vega_deposit_store';
interface PersistedDeposit {
assetId: string;
amount?: string;
}
type PersistedDepositData = Record<string, PersistedDeposit>;
const usePersistentDepositStore = create<{
deposits: PersistedDepositData;
saveValue: (entry: PersistedDeposit) => void;
lastVisited?: PersistedDeposit;
}>()(
persist(
immer((set) => ({
deposits: {},
saveValue: (entry) =>
set((state) => {
const oldValue = state.deposits[entry.assetId] || null;
state.deposits[entry.assetId] = { ...oldValue, ...entry };
state.lastVisited = { ...oldValue, ...entry };
return state;
}),
})),
{ name: STORAGE_KEY }
)
);
export const usePersistentDeposit = (
assetId?: string
): [PersistedDeposit | undefined, (entry: PersistedDeposit) => void] => {
const { deposits, lastVisited, saveValue } = usePersistentDepositStore();
const discoveredData = useMemo(() => {
return assetId
? deposits[assetId]
? deposits[assetId]
: { assetId }
: lastVisited;
}, [deposits, lastVisited, assetId]);
return [discoveredData, saveValue];
};
+3 -3
View File
@@ -1,5 +1,4 @@
import { MaxUint256 } from '@ethersproject/constants';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
import {
EthTxStatus,
useEthereumConfig,
@@ -38,9 +37,10 @@ export const useSubmitApproval = (
},
perform: () => {
if (!asset || !config) return;
const amount = removeDecimal('1000000', asset.decimals);
const id = createEthTransaction(contract, 'approve', [
config?.collateral_bridge_contract.address,
MaxUint256.toString(),
amount,
]);
setId(id);
},
-3
View File
@@ -1,4 +1 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
@@ -140,6 +140,8 @@ describe('Network switcher', () => {
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.DEVNET]: 'https://dev.net',
[Networks.STAGNET1]: 'https://stag1.net',
[Networks.SANDBOX]: 'https://sandbox.net',
[Networks.MIRROR]: 'https://mirror.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
@@ -179,6 +181,8 @@ describe('Network switcher', () => {
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.DEVNET]: 'https://dev.net',
[Networks.STAGNET1]: 'https://stag1.net',
[Networks.SANDBOX]: 'https://sandbox.net',
[Networks.MIRROR]: 'https://mirror.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
@@ -211,6 +215,8 @@ describe('Network switcher', () => {
[Networks.STAGNET3]: 'https://stag3.net',
[Networks.DEVNET]: 'https://dev.net',
[Networks.STAGNET1]: 'https://stag1.net',
[Networks.SANDBOX]: 'https://sandbox.net',
[Networks.MIRROR]: 'https://mirror.net',
};
// @ts-ignore Typescript doesn't know about this module being mocked
useEnvironment.mockImplementation(() => ({
@@ -17,10 +17,12 @@ export const envNameMapping: Record<Networks, string> = {
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
[Networks.CUSTOM]: t('Custom'),
[Networks.DEVNET]: t('Devnet'),
[Networks.SANDBOX]: t('Sandbox'),
[Networks.STAGNET1]: t('Stagnet'),
[Networks.STAGNET3]: t('Stagnet3'),
[Networks.TESTNET]: t('Fairground testnet'),
[Networks.MAINNET]: t('Mainnet'),
[Networks.MIRROR]: t('Mainnet mirror'),
};
export const envTriggerMapping: Record<Networks, string> = {
@@ -30,6 +32,7 @@ export const envTriggerMapping: Record<Networks, string> = {
export const envDescriptionMapping: Record<Networks, string> = {
[Networks.CUSTOM]: '',
[Networks.SANDBOX]: t('A playground test environment'),
[Networks.VALIDATOR_TESTNET]: t('The validator deployed testnet'),
[Networks.DEVNET]: t('The latest Vega code auto-deployed'),
[Networks.STAGNET1]: t('A release candidate for the staging environment'),
@@ -38,6 +41,9 @@ export const envDescriptionMapping: Record<Networks, string> = {
'Public testnet run by the Vega team, often used for incentives'
),
[Networks.MAINNET]: t('The vega mainnet'),
[Networks.MIRROR]: t(
'A mirror of the mainnet environment running on an Ethereum test network'
),
};
const standardNetworkKeys = [Networks.MAINNET, Networks.TESTNET];
@@ -1,6 +1,6 @@
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import { act, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
import type {
BlockTimeSubscription,
@@ -9,7 +9,6 @@ import type {
import { BlockTimeDocument } from '../../utils/__generated__/Node';
import { StatisticsDocument } from '../../utils/__generated__/Node';
import type { RowDataProps } from './row-data';
import { POLL_INTERVAL } from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
@@ -26,7 +25,7 @@ const statsQueryMock: MockedResponse<StatisticsQuery> = {
result: {
data: {
statistics: {
blockHeight: '1234', // the actual value used in the component is the value from the header store
blockHeight: '1234',
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
@@ -252,105 +251,4 @@ describe('RowData', () => {
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
});
it('should poll the query unless an errors is returned', async () => {
jest.useFakeTimers();
const createStatsQueryMock = (
blockHeight: string
): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: {
statistics: {
blockHeight,
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
},
},
};
};
const createFailedStatsQueryMock = (): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: undefined,
},
error: new Error('failed'),
};
};
mockHeaders(props.url);
const statsQueryMock1 = createStatsQueryMock('1234');
const statsQueryMock2 = createStatsQueryMock('1235');
const statsQueryMock3 = createFailedStatsQueryMock();
const statsQueryMock4 = createStatsQueryMock('1236');
render(
<MockedProvider
mocks={[
statsQueryMock1,
statsQueryMock2,
statsQueryMock3,
statsQueryMock4,
subMock,
]}
>
<RadioGroup>
{/* Radio group required as radio is being render in isolation */}
<RowData {...props} />
</RadioGroup>
</MockedProvider>
);
expect(screen.getByTestId('block-height-cell')).toHaveTextContent(
'Checking'
);
// statsQueryMock1 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1234');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock2 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1235');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock3 should FAIL!
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
// run the timer again, but statsQueryMock4's result should not be
// rendered even though its successful, because the poll
// should have been stopped
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// should still render the result of statsQueryMock3
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
jest.useRealTimers();
});
});
@@ -11,7 +11,7 @@ import {
} from '../../utils/__generated__/Node';
import { LayoutCell } from './layout-cell';
export const POLL_INTERVAL = 1000;
const POLL_INTERVAL = 1000;
export const BLOCK_THRESHOLD = 3;
export interface RowDataProps {
@@ -60,22 +60,18 @@ export const RowData = ({
// handle polling
useEffect(() => {
const handleStartPoll = () => {
if (error) return;
startPolling(POLL_INTERVAL);
};
const handleStartPoll = () => startPolling(POLL_INTERVAL);
const handleStopPoll = () => stopPolling();
// start polling on mount, but only if there is no error
if (error) {
handleStopPoll();
} else {
handleStartPoll();
}
window.addEventListener('blur', handleStopPoll);
window.addEventListener('focus', handleStartPoll);
handleStartPoll();
if (error) {
stopPolling();
}
return () => {
window.removeEventListener('blur', handleStopPoll);
window.removeEventListener('focus', handleStartPoll);
@@ -85,7 +81,6 @@ export const RowData = ({
// measure response time
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
// every time we get data measure response speed
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
@@ -182,14 +177,7 @@ export const RowData = ({
hasError={getHasError()}
dataTestId="block-height-cell"
>
<span
data-testid="query-block-height"
data-query-block-height={
error ? 'failed' : data?.statistics.blockHeight
}
>
{getBlockDisplayValue(headers?.blockHeight, error)}
</span>
{getBlockDisplayValue(headers?.blockHeight, error)}
</LayoutCell>
<LayoutCell
label={t('Subscription')}
+3 -2
View File
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { Networks } from '../types';
import { useEnvironment } from './use-environment';
type Net = Exclude<Networks, 'CUSTOM'>;
type Net = Exclude<Networks, 'CUSTOM' | 'SANDBOX'>;
export enum DApp {
Explorer = 'Explorer',
Console = 'Console',
@@ -21,6 +21,7 @@ const EmptyLinks: DAppLinks = {
[Networks.STAGNET3]: '',
[Networks.TESTNET]: '',
[Networks.MAINNET]: '',
[Networks.MIRROR]: '',
};
const ExplorerLinks = {
@@ -60,7 +61,7 @@ export const useLinks = (dapp: DApp, network?: Net) => {
};
let net = network || VEGA_ENV;
if (net === Networks.CUSTOM) {
if (net === Networks.CUSTOM || net === Networks.SANDBOX) {
net = Networks.TESTNET;
}
+2
View File
@@ -5,11 +5,13 @@ import type { envSchema } from './utils/validate-environment';
export enum Networks {
VALIDATOR_TESTNET = 'VALIDATOR_TESTNET',
CUSTOM = 'CUSTOM',
SANDBOX = 'SANDBOX',
TESTNET = 'TESTNET',
STAGNET1 = 'STAGNET1',
STAGNET3 = 'STAGNET3',
DEVNET = 'DEVNET',
MAINNET = 'MAINNET',
MIRROR = 'MIRROR',
}
export type Environment = z.infer<typeof envSchema>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
@@ -3,11 +3,13 @@ import z from 'zod';
export enum Networks {
VALIDATOR_TESTNET = 'VALIDATOR_TESTNET',
CUSTOM = 'CUSTOM',
SANDBOX = 'SANDBOX',
TESTNET = 'TESTNET',
STAGNET1 = 'STAGNET1',
STAGNET3 = 'STAGNET3',
DEVNET = 'DEVNET',
MAINNET = 'MAINNET',
MIRROR = 'MIRROR',
}
const schemaObject = {
@@ -103,3 +103,13 @@ query LiquidityProviderFeeShare($marketId: ID!) {
}
}
}
subscription LiquidityProviderFeeShareUpdate($marketId: ID!) {
marketsData(marketIds: [$marketId]) {
liquidityProviderFeeShare {
partyId
equityLikeShare
averageEntryValuation
}
}
}
+42 -1
View File
@@ -36,6 +36,13 @@ export type LiquidityProviderFeeShareQueryVariables = Types.Exact<{
export type LiquidityProviderFeeShareQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, data?: { __typename?: 'MarketData', market: { __typename?: 'Market', id: string }, liquidityProviderFeeShare?: Array<{ __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, party: { __typename?: 'Party', id: string } }> | null } | null } | null };
export type LiquidityProviderFeeShareUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type LiquidityProviderFeeShareUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', liquidityProviderFeeShare?: Array<{ __typename?: 'ObservableLiquidityProviderFeeShare', partyId: string, equityLikeShare: string, averageEntryValuation: string }> | null }> };
export const LiquidityProvisionFieldsFragmentDoc = gql`
fragment LiquidityProvisionFields on LiquidityProvision {
party {
@@ -249,4 +256,38 @@ export function useLiquidityProviderFeeShareLazyQuery(baseOptions?: Apollo.LazyQ
}
export type LiquidityProviderFeeShareQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareQuery>;
export type LiquidityProviderFeeShareLazyQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareLazyQuery>;
export type LiquidityProviderFeeShareQueryResult = Apollo.QueryResult<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>;
export type LiquidityProviderFeeShareQueryResult = Apollo.QueryResult<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>;
export const LiquidityProviderFeeShareUpdateDocument = gql`
subscription LiquidityProviderFeeShareUpdate($marketId: ID!) {
marketsData(marketIds: [$marketId]) {
liquidityProviderFeeShare {
partyId
equityLikeShare
averageEntryValuation
}
}
}
`;
/**
* __useLiquidityProviderFeeShareUpdateSubscription__
*
* To run a query within a React component, call `useLiquidityProviderFeeShareUpdateSubscription` and pass it any options that fit your needs.
* When your component renders, `useLiquidityProviderFeeShareUpdateSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLiquidityProviderFeeShareUpdateSubscription({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useLiquidityProviderFeeShareUpdateSubscription(baseOptions: Apollo.SubscriptionHookOptions<LiquidityProviderFeeShareUpdateSubscription, LiquidityProviderFeeShareUpdateSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LiquidityProviderFeeShareUpdateSubscription, LiquidityProviderFeeShareUpdateSubscriptionVariables>(LiquidityProviderFeeShareUpdateDocument, options);
}
export type LiquidityProviderFeeShareUpdateSubscriptionHookResult = ReturnType<typeof useLiquidityProviderFeeShareUpdateSubscription>;
export type LiquidityProviderFeeShareUpdateSubscriptionResult = Apollo.SubscriptionResult<LiquidityProviderFeeShareUpdateSubscription>;
@@ -6,6 +6,7 @@ import produce from 'immer';
import {
LiquidityProviderFeeShareDocument,
LiquidityProviderFeeShareUpdateDocument,
LiquidityProvisionsDocument,
LiquidityProvisionsUpdateDocument,
MarketLpDocument,
@@ -17,6 +18,7 @@ import type {
LiquidityProviderFeeShareFieldsFragment,
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareQueryVariables,
LiquidityProviderFeeShareUpdateSubscription,
LiquidityProvisionFieldsFragment,
LiquidityProvisionsQuery,
LiquidityProvisionsQueryVariables,
@@ -113,14 +115,42 @@ export const marketLiquidityDataProvider = makeDataProvider<
export const liquidityFeeShareDataProvider = makeDataProvider<
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareFieldsFragment[],
never,
never,
LiquidityProviderFeeShareUpdateSubscription,
LiquidityProviderFeeShareUpdateSubscription['marketsData'][0]['liquidityProviderFeeShare'],
LiquidityProviderFeeShareQueryVariables
>({
query: LiquidityProviderFeeShareDocument,
subscriptionQuery: LiquidityProviderFeeShareUpdateDocument,
update: (
data: LiquidityProviderFeeShareFieldsFragment[] | null,
deltas: LiquidityProviderFeeShareUpdateSubscription['marketsData'][0]['liquidityProviderFeeShare']
) => {
return produce(data || [], (draft) => {
deltas?.forEach((delta) => {
const id = delta.partyId;
const index = draft.findIndex((a) => a.party.id === id);
if (index !== -1) {
draft[index].equityLikeShare = delta.equityLikeShare;
draft[index].averageEntryValuation = delta.averageEntryValuation;
} else {
draft.unshift({
equityLikeShare: delta.equityLikeShare,
averageEntryValuation: delta.averageEntryValuation,
party: {
id: delta.partyId,
},
// TODO add accounts connection to the subscription
});
}
});
});
},
getData: (data) => {
return data?.market?.data?.liquidityProviderFeeShare || [];
},
getDelta: (subscriptionData: LiquidityProviderFeeShareUpdateSubscription) => {
return subscriptionData.marketsData[0].liquidityProviderFeeShare;
},
});
export type Filter = { partyId?: string; active?: boolean };
+20 -15
View File
@@ -1,6 +1,8 @@
import React from 'react';
import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook';
import { addDecimal } from '@vegaprotocol/utils';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { marketDepthProvider } from './market-depth-provider';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
@@ -46,12 +48,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
throttle(() => {
dataRef.current = {
...marketDataRef.current,
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
),
indicativePrice: marketDataRef.current?.indicativePrice
? getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
)
: undefined,
midPrice: getMidPrice(
rawDataRef.current?.depth.sell,
rawDataRef.current?.depth.buy,
@@ -146,12 +148,13 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
variables,
});
if (!marketDataRef.current && marketData) {
marketDataRef.current = marketData;
}
marketDataRef.current = marketData;
useEffect(() => {
const throttleRunner = updateOrderbookData.current;
if (!marketDataRef.current) {
return;
}
if (!data) {
dataRef.current = { rows: null };
setOrderbookData(dataRef.current);
@@ -159,9 +162,10 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
}
dataRef.current = {
...marketDataRef.current,
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(marketDataRef.current.indicativePrice, resolution),
indicativePrice: getPriceLevel(
marketDataRef.current.indicativePrice,
resolution
),
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
};
@@ -171,7 +175,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
return () => {
throttleRunner.cancel();
};
}, [data, resolution]);
}, [data, marketData, resolution]);
useEffect(() => {
resolutionRef.current = resolution;
@@ -193,9 +197,10 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
resolution={resolution}
onResolutionChange={(resolution: number) => setResolution(resolution)}
onClick={(price: string) => {
onClick={(price?: string | number) => {
if (price) {
updateOrder(marketId, { price });
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
updateOrder(marketId, { price: priceValue });
}
}}
/>
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { PriceCell, VolCell, CumulativeVol } from '@vegaprotocol/datagrid';
interface OrderbookRowProps {
@@ -15,7 +15,7 @@ interface OrderbookRowProps {
price: string;
relativeAsk?: number;
relativeBid?: number;
onClick?: (price: string) => void;
onClick?: (price?: string | number) => void;
}
export const OrderbookRow = React.memo(
@@ -59,7 +59,7 @@ export const OrderbookRow = React.memo(
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() => onClick && onClick(addDecimal(price, decimalPlaces))}
onClick={() => onClick && onClick(price)}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
/>
<CumulativeVol
+9 -50
View File
@@ -20,7 +20,7 @@ describe('Orderbook', () => {
const decimalPlaces = 3;
it('should scroll to mid price on init', async () => {
window.innerHeight = 11 * rowHeight;
render(
const result = render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
@@ -30,7 +30,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
});
it('should keep mid price row in the middle', async () => {
@@ -45,7 +45,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
result.rerender(
<Orderbook
decimalPlaces={decimalPlaces}
@@ -121,7 +121,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
});
it('should get back to mid price on click', async () => {
@@ -143,7 +143,7 @@ describe('Orderbook', () => {
expect(result.getByTestId('scroll').scrollTop).toBe(1);
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
fireEvent.click(scrollToMidPriceButton);
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
});
it('should get back to mid price on resolution change', async () => {
@@ -158,12 +158,12 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
const scrollElement = screen.getByTestId('scroll');
const scrollElement = result.getByTestId('scroll');
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
scrollElement.scrollTop = 1;
fireEvent.scroll(scrollElement);
expect(screen.getByTestId('scroll').scrollTop).toBe(1);
const resolutionSelect = screen.getByTestId(
expect(result.getByTestId('scroll').scrollTop).toBe(1);
const resolutionSelect = result.getByTestId(
'resolution'
) as HTMLSelectElement;
fireEvent.change(resolutionSelect, { target: { value: '10' } });
@@ -181,47 +181,6 @@ describe('Orderbook', () => {
onResolutionChange={onResolutionChange}
/>
);
expect(screen.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
});
it('should format correctly the numbers on resolution change', async () => {
const onClickSpy = jest.fn();
const result = render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
onClick={onClickSpy}
fillGaps
{...generateMockData(params)}
onResolutionChange={onResolutionChange}
/>
);
expect(
await screen.findByTestId(`bid-vol-${params.midPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await fireEvent.click(await screen.getByTestId('price-122934'));
expect(onClickSpy).toBeCalledWith('122.934');
const resolutionSelect = screen.getByTestId(
'resolution'
) as HTMLSelectElement;
await fireEvent.change(resolutionSelect, { target: { value: '10' } });
await result.rerender(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
onClick={onClickSpy}
fillGaps
{...generateMockData({
...params,
resolution: 10,
})}
onResolutionChange={onResolutionChange}
/>
);
await fireEvent.click(await screen.getByTestId('price-12299'));
// After resolution change the price is 122.99
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
expect(onClickSpy).toBeCalledWith('122.99');
expect(result.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
});
});
+1 -1
View File
@@ -21,7 +21,7 @@ interface OrderbookProps extends OrderbookData {
positionDecimalPlaces: number;
resolution: number;
onResolutionChange: (resolution: number) => void;
onClick?: (price: string) => void;
onClick?: (price?: string | number) => void;
fillGaps?: boolean;
}
@@ -169,7 +169,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
<LiquidityInfoPanel market={market}>
<Link
to={`/liquidity/${market.id}`}
onClick={(ev) => onSelect?.(market.id, ev.metaKey || ev.ctrlKey)}
onClick={(ev) => onSelect?.(market.id, ev.metaKey)}
data-testid="view-liquidity-link"
>
<UILink>{t('View liquidity provision table')}</UILink>
@@ -11,13 +11,12 @@ import {
AgGridDynamic as AgGrid,
PriceFlashCell,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import type { MarketMaybeWithData } from '../../';
import type { MarketMaybeWithData, MarketFieldsFragment } from '../../';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
const { MarketTradingMode, AuctionTrigger } = Schema;
@@ -34,6 +33,7 @@ export const MarketListTable = forwardRef<
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')}
getRowId={getRowId}
ref={ref}
defaultColDef={{
@@ -43,7 +43,7 @@ export const MarketListTable = forwardRef<
filter: true,
filterParams: { buttons: ['reset'] },
}}
suppressCellFocus
suppressCellFocus={true}
components={{ PriceFlashCell, MarketNameCell }}
{...props}
>
@@ -59,11 +59,11 @@ export const MarketListTable = forwardRef<
/>
<AgGridColumn
headerName={t('Trading mode')}
field="tradingMode"
field="data"
minWidth={170}
valueFormatter={({
valueGetter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
}: VegaValueGetterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
const { trigger } = data.data;
const { tradingMode } = data;
@@ -75,22 +75,14 @@ export const MarketListTable = forwardRef<
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[tradingMode];
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketTradingModeMapping,
}}
/>
<AgGridColumn
headerName={t('Status')}
field="state"
valueFormatter={({
valueGetter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketStateMapping,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data?.state] : '-';
}}
/>
<AgGridColumn
@@ -1,6 +1,4 @@
import type { MouseEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { t } from '@vegaprotocol/i18n';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { MarketListTable } from './market-list-table';
@@ -14,24 +12,15 @@ interface MarketsContainerProps {
}
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = useDataProvider({
dataProvider,
skipUpdates: true,
variables: undefined,
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const onFilterChanged = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
return (
<div className="h-full relative">
<MarketListTable
ref={gridRef}
rowData={data}
rowData={error ? [] : data}
suppressLoadingOverlay
suppressNoRowsOverlay
onCellClicked={(cellEvent: CellClickedEvent) => {
@@ -47,12 +36,10 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
}
onSelect(
(data as MarketMaybeWithData).id,
(event as unknown as MouseEvent)?.metaKey ||
(event as unknown as MouseEvent)?.ctrlKey
(event as unknown as MouseEvent)?.metaKey
);
}}
onMarketClick={onSelect}
onFilterChanged={onFilterChanged}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@@ -60,7 +47,6 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
error={error}
data={data}
noDataMessage={t('No markets')}
noDataCondition={() => !dataCount}
reload={reload}
/>
</div>
@@ -32,16 +32,12 @@ query OrderById($orderId: ID!) {
query Orders(
$partyId: ID!
$marketIds: [ID!]
$pagination: Pagination
$filter: OrderFilter
$filter: OrderByMarketIdsFilter
) {
party(id: $partyId) {
id
ordersConnection(
pagination: $pagination
filter: { order: $filter, marketIds: $marketIds }
) {
ordersConnection(pagination: $pagination, filter: $filter) {
edges {
node {
...OrderFields
@@ -78,8 +74,8 @@ fragment OrderUpdateFields on OrderUpdate {
}
}
subscription OrdersUpdate($partyId: ID!, $marketIds: [ID!]) {
orders(filter: { partyIds: [$partyId], marketIds: $marketIds }) {
subscription OrdersUpdate($partyId: ID!) {
orders(filter: { partyIds: [$partyId] }) {
...OrderUpdateFields
}
}
@@ -14,9 +14,8 @@ export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: '
export type OrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
pagination?: Types.InputMaybe<Types.Pagination>;
filter?: Types.InputMaybe<Types.OrderFilter>;
filter?: Types.InputMaybe<Types.OrderByMarketIdsFilter>;
}>;
@@ -26,7 +25,6 @@ export type OrderUpdateFieldsFragment = { __typename?: 'OrderUpdate', id: string
export type OrdersUpdateSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
@@ -116,13 +114,10 @@ export type OrderByIdQueryHookResult = ReturnType<typeof useOrderByIdQuery>;
export type OrderByIdLazyQueryHookResult = ReturnType<typeof useOrderByIdLazyQuery>;
export type OrderByIdQueryResult = Apollo.QueryResult<OrderByIdQuery, OrderByIdQueryVariables>;
export const OrdersDocument = gql`
query Orders($partyId: ID!, $marketIds: [ID!], $pagination: Pagination, $filter: OrderFilter) {
query Orders($partyId: ID!, $pagination: Pagination, $filter: OrderByMarketIdsFilter) {
party(id: $partyId) {
id
ordersConnection(
pagination: $pagination
filter: {order: $filter, marketIds: $marketIds}
) {
ordersConnection(pagination: $pagination, filter: $filter) {
edges {
node {
...OrderFields
@@ -153,7 +148,6 @@ export const OrdersDocument = gql`
* const { data, loading, error } = useOrdersQuery({
* variables: {
* partyId: // value for 'partyId'
* marketIds: // value for 'marketIds'
* pagination: // value for 'pagination'
* filter: // value for 'filter'
* },
@@ -171,8 +165,8 @@ export type OrdersQueryHookResult = ReturnType<typeof useOrdersQuery>;
export type OrdersLazyQueryHookResult = ReturnType<typeof useOrdersLazyQuery>;
export type OrdersQueryResult = Apollo.QueryResult<OrdersQuery, OrdersQueryVariables>;
export const OrdersUpdateDocument = gql`
subscription OrdersUpdate($partyId: ID!, $marketIds: [ID!]) {
orders(filter: {partyIds: [$partyId], marketIds: $marketIds}) {
subscription OrdersUpdate($partyId: ID!) {
orders(filter: {partyIds: [$partyId]}) {
...OrderUpdateFields
}
}
@@ -191,7 +185,6 @@ export const OrdersUpdateDocument = gql`
* const { data, loading, error } = useOrdersUpdateSubscription({
* variables: {
* partyId: // value for 'partyId'
* marketIds: // value for 'marketIds'
* },
* });
*/
@@ -112,7 +112,9 @@ describe('order data provider', () => {
const updatedData = update(data, delta, () => null, {
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
order: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
},
});
expect(
@@ -35,40 +35,45 @@ const orderMatchFilters = (
return true;
}
if (
variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status))
variables?.filter?.order?.status &&
!(order.status && variables.filter.order.status.includes(order.status))
) {
return false;
}
if (
variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type))
variables?.filter?.order?.types &&
!(order.type && variables.filter.order.types.includes(order.type))
) {
return false;
}
if (
variables?.filter?.timeInForce &&
!variables.filter.timeInForce.includes(order.timeInForce)
variables?.filter?.order?.timeInForce &&
!variables.filter.order.timeInForce.includes(order.timeInForce)
) {
return false;
}
if (variables?.filter?.excludeLiquidity && order.liquidityProvisionId) {
if (
variables?.filter?.order?.excludeLiquidity &&
order.liquidityProvisionId
) {
return false;
}
if (
variables?.filter?.dateRange?.start &&
variables?.filter?.order?.dateRange?.start &&
!(
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.start < (order.updatedAt || order.createdAt)
variables.filter.order.dateRange.start <
(order.updatedAt || order.createdAt)
)
) {
return false;
}
if (
variables?.filter?.dateRange?.end &&
variables?.filter?.order?.dateRange?.end &&
!(
(order.updatedAt || order.createdAt) &&
variables.filter.dateRange.end > (order.updatedAt || order.createdAt)
variables.filter.order.dateRange.end >
(order.updatedAt || order.createdAt)
)
) {
return false;
@@ -238,17 +243,18 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
{ partyId: string; marketId?: string }
>(
[
(callback, client, { partyId, marketId }) =>
(callback, client, variables) =>
hasActiveOrderProviderInternal(callback, client, {
marketIds: marketId ? [marketId] : undefined,
filter: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
order: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
},
},
pagination: {
first: 1,
},
partyId,
...variables,
} as OrdersQueryVariables),
],
(parts) => parts[0]
@@ -72,10 +72,12 @@ export const useOrderListData = ({
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId,
filter: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
order: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
},
},
pagination: {
first: 1000,
+3
View File
@@ -1,5 +1,8 @@
export * from './__generated__/OrdersSubscription';
export * from './use-has-active-order';
export * from './use-order-cancel';
export * from './use-order-edit';
export * from './use-order-submit';
export * from './use-order-update';
export * from './use-pending-orders-volume';
export * from './use-order-store';
@@ -0,0 +1,138 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import type { ReactNode } from 'react';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { useOrderCancel } from './use-order-cancel';
import type { OrderSubSubscription } from './';
import { OrderSubDocument } from './';
import * as Schema from '@vegaprotocol/types';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderCancel(), { wrapper });
}
describe('useOrderCancel', () => {
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.cancel).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
act(() => {
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should cancel a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
const args = {
orderId: 'order-id',
marketId: 'market-id',
};
act(() => {
result.current.cancel(args);
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderCancellation: args,
});
});
});
@@ -0,0 +1,83 @@
import { useCallback, useState } from 'react';
import {
useVegaWallet,
useVegaTransaction,
useTransactionResult,
} from '@vegaprotocol/wallet';
import type {
OrderCancellationBody,
TransactionResult,
} from '@vegaprotocol/wallet';
import type { OrderSubFieldsFragment } from './';
import * as Sentry from '@sentry/react';
import { useOrderUpdate } from './use-order-update';
export const useOrderCancel = () => {
const { pubKey } = useVegaWallet();
const [cancelledOrder, setCancelledOrder] =
useState<OrderSubFieldsFragment | null>(null);
const [transactionResult, setTransactionResult] =
useState<TransactionResult>();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const waitForTransactionResult = useTransactionResult();
const reset = useCallback(() => {
resetTransaction();
setCancelledOrder(null);
}, [resetTransaction]);
const cancel = useCallback(
async (orderCancellation: OrderCancellationBody['orderCancellation']) => {
if (!pubKey) {
return;
}
setCancelledOrder(null);
try {
const res = await send(pubKey, {
orderCancellation,
});
if (orderCancellation.orderId) {
const cancelledOrder = await waitForOrderUpdate(
orderCancellation.orderId,
pubKey
);
setCancelledOrder(cancelledOrder);
setComplete();
} else if (res) {
const txResult = await waitForTransactionResult(
res.transactionHash,
pubKey
);
setTransactionResult(txResult);
setComplete();
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[pubKey, send, setComplete, waitForOrderUpdate, waitForTransactionResult]
);
return {
transaction,
transactionResult,
cancelledOrder,
Dialog,
cancel,
reset,
};
};
@@ -0,0 +1,179 @@
import { act, renderHook } from '@testing-library/react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { useOrderEdit } from './use-order-edit';
import type { OrderSubSubscription } from './__generated__/OrdersSubscription';
import { OrderSubDocument } from './__generated__/OrdersSubscription';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { Order } from '../components';
import { generateOrder } from '../components';
import * as Schema from '@vegaprotocol/types';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(order: Order, context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderEdit(order), { wrapper });
}
describe('useOrderEdit', () => {
it('should edit a correctly formatted order if there is no size', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const order = generateOrder({
price: '123456789',
market: { decimalPlaces: 2 },
});
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
act(() => {
result.current.edit({ price: '1234567.89' });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderAmendment: {
orderId: order.id,
// eslint-disable-next-line
marketId: order.market!.id,
timeInForce: order.timeInForce,
price: '123456789', // Decimal removed
sizeDelta: 0,
expiresAt: undefined,
},
});
});
it('should edit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const order = generateOrder({
price: '123456789',
market: { decimalPlaces: 2 },
});
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
act(() => {
result.current.edit({ price: '1234567.89', size: '20' });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderAmendment: {
orderId: order.id,
// eslint-disable-next-line
marketId: order.market!.id,
timeInForce: order.timeInForce,
price: '123456789', // Decimal removed
sizeDelta: 1990,
expiresAt: undefined,
},
});
});
it('has the correct default state', () => {
const order = generateOrder();
const { result } = setup(order);
expect(typeof result.current.edit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const order = generateOrder();
const mockSendTx = jest.fn();
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
await act(async () => {
result.current.edit(order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,82 @@
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import { useState, useCallback } from 'react';
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import type { OrderSubFieldsFragment } from './';
import * as Sentry from '@sentry/react';
import type { Order } from '../components';
import { useOrderUpdate } from './use-order-update';
import BigNumber from 'bignumber.js';
export interface EditOrderArgs {
price: string;
size?: string;
}
export const useOrderEdit = (order: Order | null) => {
const { pubKey } = useVegaWallet();
const [updatedOrder, setUpdatedOrder] =
useState<OrderSubFieldsFragment | null>(null);
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
}, [resetTransaction]);
const edit = useCallback(
async (args: EditOrderArgs) => {
if (!pubKey || !order || !order.market) {
return;
}
setUpdatedOrder(null);
try {
await send(pubKey, {
orderAmendment: {
orderId: order.id,
marketId: order.market.id,
price: removeDecimal(args.price, order.market.decimalPlaces),
timeInForce: order.timeInForce,
sizeDelta: args.size
? new BigNumber(
removeDecimal(args.size, order.market.positionDecimalPlaces)
)
.minus(order.size)
.toNumber()
: 0,
expiresAt: order.expiresAt
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
: undefined,
},
});
const updatedOrder = await waitForOrderUpdate(order.id, pubKey);
setUpdatedOrder(updatedOrder);
setComplete();
} catch (e) {
Sentry.captureException(e);
return;
}
},
[pubKey, send, order, setComplete, waitForOrderUpdate]
);
return {
transaction,
updatedOrder,
Dialog,
edit,
reset,
};
};
@@ -0,0 +1,205 @@
import { act, renderHook } from '@testing-library/react';
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useOrderSubmit } from './use-order-submit';
import type { OrderSubSubscription } from './';
import { OrderSubDocument } from './';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
const marketId = 'market-id';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderSubmit(), { wrapper });
}
describe('useOrderSubmit', () => {
it('should submit a correctly formatted order on GTT', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKey = '0x123';
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [{ publicKey: pubKey, name: 'test key 1' }],
pubKey,
});
const order = {
type: Schema.OrderType.TYPE_LIMIT,
size: '10',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
side: Schema.Side.SIDE_BUY,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
};
await act(async () => {
result.current.submit({ ...order, marketId });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKey, {
orderSubmission: {
type: Schema.OrderType.TYPE_LIMIT,
marketId,
size: '10',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
},
});
});
it('should submit a correctly formatted order on GTC', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const publicKeyObj: PubKey = {
publicKey: '0x123',
name: 'test key 1',
};
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [publicKeyObj],
pubKey: publicKeyObj.publicKey,
});
const order = {
type: Schema.OrderType.TYPE_LIMIT,
size: '10',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
};
await act(async () => {
result.current.submit({ ...order, marketId });
});
expect(mockSendTx).toHaveBeenCalledWith(publicKeyObj.publicKey, {
orderSubmission: {
type: Schema.OrderType.TYPE_LIMIT,
marketId,
size: '10',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
},
});
});
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.submit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
await act(async () => {
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should not sendTx side is not specified', async () => {
const mockSendTx = jest.fn();
const publicKeyObj: PubKey = {
publicKey: '0x123',
name: 'test key 1',
};
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [publicKeyObj],
pubKey: publicKeyObj.publicKey,
});
await act(async () => {
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,142 @@
import { useCallback, useState } from 'react';
import type { ReactNode } from 'react';
import type { OrderSubFieldsFragment } from './__generated__/OrdersSubscription';
import {
useVegaWallet,
useVegaTransaction,
determineId,
} from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import { useOrderUpdate } from './use-order-update';
import * as Schema from '@vegaprotocol/types';
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
export const getOrderDialogTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order submitted');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Submission failed');
}
};
export const getOrderDialogIntent = (
status?: Schema.OrderStatus
): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return Intent.Warning;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
case Schema.OrderStatus.STATUS_CANCELLED:
return Intent.Danger;
case Schema.OrderStatus.STATUS_FILLED:
case Schema.OrderStatus.STATUS_ACTIVE:
return Intent.Success;
default:
return;
}
};
export const getOrderDialogIcon = (
status?: Schema.OrderStatus
): ReactNode | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
return <Icon name="warning-sign" size={16} />;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
case Schema.OrderStatus.STATUS_CANCELLED:
return <Icon name="error" size={16} />;
default:
return;
}
};
export const useOrderSubmit = () => {
const { pubKey } = useVegaWallet();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const [finalizedOrder, setFinalizedOrder] =
useState<OrderSubFieldsFragment | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
}, [resetTransaction]);
const submit = useCallback(
async (orderSubmission: OrderSubmissionBody['orderSubmission']) => {
if (!pubKey || !orderSubmission.side) {
return;
}
setFinalizedOrder(null);
try {
const res = await send(pubKey, { orderSubmission });
if (res) {
const orderId = determineId(res.signature);
if (orderId) {
const order = await waitForOrderUpdate(orderId, pubKey);
setFinalizedOrder(order);
setComplete();
}
}
} catch (e) {
Sentry.captureException(e);
}
},
[pubKey, send, setComplete, waitForOrderUpdate]
);
return {
transaction,
finalizedOrder,
Dialog,
submit,
reset,
};
};
@@ -53,12 +53,14 @@ export const useActiveOrdersVolumeAndMargin = (
update,
variables: {
partyId: partyId || '',
marketIds: [marketId],
filter: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
marketIds: [marketId],
order: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
},
},
},
skip: !partyId,
-129
View File
@@ -1,129 +0,0 @@
import { Intent } from '@vegaprotocol/ui-toolkit';
import {
getOrderToastIntent,
getOrderToastTitle,
getRejectionReason,
timeInForceLabel,
} from './utils';
import * as Types from '@vegaprotocol/types';
describe('getOrderToastTitle', () => {
it('should return the correct title', () => {
expect(getOrderToastTitle(Types.OrderStatus.STATUS_ACTIVE)).toBe(
'Order submitted'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_FILLED)).toBe(
'Order filled'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_PARTIALLY_FILLED)).toBe(
'Order partially filled'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_PARKED)).toBe(
'Order parked'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_STOPPED)).toBe(
'Order stopped'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_CANCELLED)).toBe(
'Order cancelled'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_EXPIRED)).toBe(
'Order expired'
);
expect(getOrderToastTitle(Types.OrderStatus.STATUS_REJECTED)).toBe(
'Order rejected'
);
expect(getOrderToastTitle(undefined)).toBe(undefined);
});
});
describe('getOrderToastIntent', () => {
it('should return the correct intent', () => {
expect(getOrderToastIntent(Types.OrderStatus.STATUS_PARKED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_EXPIRED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_PARTIALLY_FILLED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_REJECTED)).toBe(
Intent.Danger
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_STOPPED)).toBe(
Intent.Danger
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_FILLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_ACTIVE)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_CANCELLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(undefined)).toBe(undefined);
});
});
describe('getRejectionReason', () => {
it('should return the correct rejection reason for insufficient asset balance', () => {
expect(
getRejectionReason({
rejectionReason:
Types.OrderRejectionReason.ORDER_ERROR_INSUFFICIENT_ASSET_BALANCE,
status: Types.OrderStatus.STATUS_REJECTED,
id: '',
createdAt: undefined,
size: '',
price: '',
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
side: Types.Side.SIDE_BUY,
marketId: '',
})
).toBe('Insufficient asset balance');
});
it('should return the correct rejection reason when order is stopped', () => {
expect(
getRejectionReason({
rejectionReason: null,
status: Types.OrderStatus.STATUS_STOPPED,
id: '',
createdAt: undefined,
size: '',
price: '',
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
side: Types.Side.SIDE_BUY,
marketId: '',
})
).toBe(
'Your Fill or Kill (FOK) order was not filled and it has been stopped'
);
});
});
describe('timeInForceLabel', () => {
it('should return the correct label for time in force', () => {
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(
`Fill or Kill (FOK)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(
`Good 'til Cancelled (GTC)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(
`Immediate or Cancel (IOC)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(
`Good 'til Time (GTT)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(
`Good for Auction (GFA)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(
`Good for Normal (GFN)`
);
expect(timeInForceLabel('')).toBe('');
});
});
-53
View File
@@ -1,7 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import type { OrderSubFieldsFragment } from './order-hooks';
import { Intent } from '@vegaprotocol/ui-toolkit';
// More detail in https://docs.vega.xyz/mainnet/graphql/enums/order-time-in-force
export const timeInForceLabel = (tif: string) => {
@@ -39,55 +38,3 @@ export const getRejectionReason = (
: null;
}
};
export const getOrderToastTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order submitted');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Submission failed');
}
};
export const getOrderToastIntent = (
status?: Schema.OrderStatus
): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return Intent.Warning;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
return Intent.Danger;
case Schema.OrderStatus.STATUS_FILLED:
case Schema.OrderStatus.STATUS_ACTIVE:
case Schema.OrderStatus.STATUS_CANCELLED:
return Intent.Success;
default:
return;
}
};
+2
View File
@@ -4,6 +4,8 @@ export * from './lib/positions-data-providers';
export * from './lib/margin-data-provider';
export * from './lib/margin-calculator';
export * from './lib/positions-table';
export * from './lib/use-close-position';
export * from './lib/use-market-margin';
export * from './lib/use-market-position-open-volume';
export * from './lib/use-open-volume';
export * from './lib/use-positions-data';
@@ -0,0 +1,109 @@
import { useEnvironment } from '@vegaprotocol/environment';
import type { OrderFieldsFragment } from '@vegaprotocol/orders';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { Link } from '@vegaprotocol/ui-toolkit';
import type { TransactionResult, VegaTxState } from '@vegaprotocol/wallet';
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
import { useRequestClosePositionData } from '../use-request-close-position-data';
import { ClosingOrder } from './shared';
interface CompleteProps {
partyId: string;
transaction: VegaTxState;
transactionResult?: TransactionResult;
closingOrder?: IClosingOrder;
closingOrderResult?: OrderFieldsFragment;
}
export const Complete = ({
partyId,
transaction,
transactionResult,
closingOrder,
closingOrderResult,
}: CompleteProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
if (!transactionResult || !closingOrderResult) return null;
return (
<>
{closingOrderResult.status === Schema.OrderStatus.STATUS_FILLED &&
transactionResult.status ? (
<Success partyId={partyId} order={closingOrder} />
) : (
<Error
transactionResult={transactionResult}
closingOrderResult={closingOrderResult}
/>
)}
{transaction.txHash && (
<>
<p className="font-semibold mt-4">{t('Transaction')}</p>
<p>
<Link
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
target="_blank"
>
{truncateByChars(transaction.txHash)}
</Link>
</p>
</>
)}
</>
);
};
const Success = ({
partyId,
order,
}: {
partyId: string;
order?: IClosingOrder;
}) => {
const { market, marketData, orders } = useRequestClosePositionData(
order?.marketId,
partyId
);
if (!market || !marketData || !orders) {
return <div>{t('Loading...')}</div>;
}
if (!order) {
return (
<div className="text-vega-pink">{t('Could retrieve closing order')}</div>
);
}
return (
<>
<h2 className="font-bold">{t('Position closed')}</h2>
<ClosingOrder order={order} market={market} marketData={marketData} />
</>
);
};
const Error = ({
transactionResult,
closingOrderResult,
}: {
transactionResult: TransactionResult;
closingOrderResult: OrderFieldsFragment;
}) => {
const reason =
closingOrderResult.rejectionReason &&
Schema.OrderRejectionReasonMapping[closingOrderResult.rejectionReason];
return (
<div className="text-vega-pink">
{reason ? (
<p>{reason}</p>
) : (
<p>
{t('Transaction failed')}: {transactionResult.error}
</p>
)}
</div>
);
};

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