Compare commits

..
Author SHA1 Message Date
Matthew Russell b4a2e7d604 feat: add reload prompt after extension is installed 2024-03-07 13:06:58 +00:00
Matthew Russell 6c7d4808e1 fix: clear full state on disconnect 2024-03-07 12:45:58 +00:00
bwallacee 3c931e180e chore(trading): fix change keys 2024-03-07 09:33:15 +00:00
Dariusz Majcherczyk d760e4cc8e fix(trading): skip flaky tests 2024-03-06 17:16:15 +01:00
Matthew Russell 6f59083169 fix: wallet check in governance bootstrap 2024-03-06 15:20:47 +00:00
Matthew Russell f0b953fb71 fix: check for on/off methods before calling 2024-03-06 15:00:51 +00:00
Matthew Russell d46af0009a fix: tests and only show error data if present 2024-03-06 13:54:59 +00:00
Matthew Russell cda3a5050e fix: ensure json check for all snap invocations, make is snap error check less strict for ff 2024-03-06 13:18:10 +00:00
Matthew Russell 76048fea66 fix: add missing hook dep in eager connect 2024-03-06 11:02:23 +00:00
Matthew Russell 5bbb592b01 fix: pipe error through from injected wallet 2024-03-06 11:02:23 +00:00
Matthew Russell 722897fdc6 fix: handle errors thrown by snap better 2024-03-06 11:02:23 +00:00
Matthew Russell 961c6e2ee6 fix: check connector status before eager connect, fix input error text wrapping 2024-03-06 11:02:23 +00:00
Matthew Russell 9e413e66ae chore: fix unnecessary calls to listKeys 2024-03-06 11:02:23 +00:00
Matthew Russell c46d701361 fix: don't use deprecated selectedWallet property in snap connector 2024-03-06 11:02:23 +00:00
Art 0d39c2354c fix(trading): add team id to games query (#5921) 2024-03-06 10:47:47 +00:00
Matthew Russell ad6f0c5798 feat(trading): add and show key aliases (#5819) 2024-03-06 10:47:16 +00:00
m.ray 38d13085fb chore(trading): remove view as pubkey banner (#5926) 2024-03-05 16:15:51 +00:00
daro-maj c0f4278b81 fix(trading): fix ci cypress issue (#5922) 2024-03-05 16:09:14 +00:00
Ben b2777043b4 chore(trading): update vega and market-sim (#5919) 2024-03-05 15:49:50 +00:00
Matthew Russell 4d19b55096 Merge pull request #5912 from vegaprotocol/main
chore(trading,governance): back merge hotfixes
2024-03-05 04:14:14 -05:00
Matthew Russellandbwallacee 7ea7362a7d fix(trading): market into tab 24hr vol and price fix (#5910)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-03-04 19:08:42 +00:00
Art bbfe42ddb1 fix(governance): rewards moved to console notification (#5904) 2024-03-04 18:09:52 +00:00
Matthew Russell b163af3e8a fix(trading): 24hr price and volume in header (#5908) 2024-03-04 18:00:54 +00:00
daro-maj e6b3ff456d fix(trading): fix tests due to new wallet connection method (#5903) 2024-03-04 15:58:33 +01:00
Matthew Russell 00dbb7dd60 chore(trading): delete trading-e2e cypress tests (#5901) 2024-03-03 14:25:04 +00:00
Matthew Russell 82df401611 fix(governance): incorrect penalty information in validators table and page (#5896) 2024-03-01 22:22:17 +00:00
78 changed files with 843 additions and 1535 deletions
-36
View File
@@ -1,36 +0,0 @@
name: Cypress Console tests -- live environment
# This workflow runs using provided url
on:
workflow_dispatch:
inputs:
url:
description: 'Url'
required: true
type: string
jobs:
cypress-run:
name: Run Cypress Trading tests -- live environment
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 20
id: Node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Run Cypress tests
uses: cypress-io/github-action@v4
with:
browser: chrome
record: true
project: ./apps/trading-e2e
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -12,7 +12,6 @@ on:
options:
- explorer-e2e
- governance-e2e
- trading-e2e
tags:
description: 'Test tags to run'
required: true
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
projects: '["explorer-e2e","governance-e2e"]'
tags: '@smoke @regression @slow'
@@ -7,6 +7,7 @@ import {
navigateTo,
navigation,
turnTelemetryOff,
setRiskAccepted,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
@@ -57,6 +58,7 @@ context(
// 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
setRiskAccepted();
ethereumWalletConnect();
cy.connectVegaWallet();
vegaWalletSetSpecifiedApprovalAmount('1000');
@@ -5,6 +5,7 @@ import {
navigateTo,
navigation,
turnTelemetryOff,
setRiskAccepted,
} from '../../support/common.functions';
import {
stakingPageAssociateTokens,
@@ -57,6 +58,7 @@ context(
function () {
cy.clearLocalStorage();
turnTelemetryOff();
setRiskAccepted();
cy.mockChainId();
cy.reload();
waitForSpinner();
@@ -79,23 +79,23 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have information on active nodes', function () {
it.skip('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('contain.text', '1')
.should('contain.text', '2')
.and('contain.text', 'active nodes');
});
it('should have information on consensus nodes', function () {
it.skip('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '1')
.should('contain.text', '2')
.and('contain.text', 'consensus nodes');
});
it('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '1')
.should('have.length', '2')
.each(($validator) => {
cy.wrap($validator).find('a').should('have.attr', 'href');
});
@@ -3,6 +3,7 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
navigation,
setRiskAccepted,
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
@@ -187,6 +188,7 @@ context('Validators Page - verify elements on page', function () {
before('connect wallets and click on validator', function () {
cy.mockChainId();
cy.visit('/validators');
setRiskAccepted();
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
@@ -1,5 +1,8 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import {
setRiskAccepted,
waitForSpinner,
} from '../../support/common.functions';
import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
@@ -11,7 +14,6 @@ const connectButton = 'connect-vega-wallet';
const getVegaLink = 'link';
const dialog = '[role="dialog"]:visible';
const dialogHeader = 'dialog-title';
const walletDialogHeader = 'wallet-dialog-title';
const connectorsList = 'connectors-list';
const dialogCloseBtn = 'dialog-close';
const accountNo = 'vega-account-truncated';
@@ -34,6 +36,7 @@ context(
() => {
before('visit token home page', () => {
cy.visit('/');
setRiskAccepted();
cy.get(walletContainer, { timeout: 60000 }).should('be.visible');
});
@@ -63,17 +66,12 @@ context(
it('should have Connect Vega header visible', () => {
cy.get(dialog).within(() => {
cy.getByTestId(walletDialogHeader)
cy.getByTestId(connectorsList)
.should('be.visible')
.and('have.text', 'Get a Vega wallet');
});
});
it('should have jsonRpc and hosted connection options visible on list', function () {
cy.getByTestId(connectorsList).within(() => {
cy.getByTestId('connector-jsonRpc')
.should('be.visible')
.and('have.text', 'Use the Desktop App/CLI');
.and(
'have.text',
'Get the Vega WalletGet MetaMask>_Command Line WalletView as public key'
);
});
});
@@ -88,7 +86,6 @@ context(
before('connect vega wallet', function () {
cy.mockChainId();
cy.visit('/');
cy.wait('@ChainId');
cy.connectVegaWallet();
vegaWalletTeardown();
});
@@ -102,6 +102,12 @@ export function turnTelemetryOff() {
);
}
export function setRiskAccepted() {
cy.window().then((win) =>
win.localStorage.setItem('vega_wallet_risk_accepted', 'true')
);
}
export function dissociateFromSecondWalletKey() {
const secondWalletKey = Cypress.env('vegaWalletPublicKey2Short');
cy.getByTestId('vega-in-wallet')
+4 -2
View File
@@ -26,9 +26,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const { token, staking, vesting } = useContracts();
const setAssociatedBalances = useRefreshAssociatedBalances();
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
const vegaConnecting = useEagerConnect();
const vegaWalletStatus = useEagerConnect();
const loaded = balancesLoaded && !vegaConnecting;
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting';
React.useEffect(() => {
const run = async () => {
@@ -169,3 +169,5 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
}
return <Suspense fallback={loading}>{children}</Suspense>;
};
AppLoader.displayName = 'AppLoader';
@@ -0,0 +1,44 @@
import {
useLinks,
DApp,
CONSOLE_REWARDS_PAGE,
} from '@vegaprotocol/environment';
import {
ExternalLink,
Intent,
NotificationBanner,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Trans } from 'react-i18next';
import { useMatch } from 'react-router-dom';
import Routes from '../../routes/routes';
import { type ReactNode } from 'react';
const ConsoleRewardsLink = ({ children }: { children: ReactNode }) => {
const consoleLink = useLinks(DApp.Console);
return (
<ExternalLink
href={consoleLink(CONSOLE_REWARDS_PAGE)}
className="underline inline-flex gap-1 items-center"
title="Rewards in Console"
>
<span>{children}</span>
<VegaIcon size={12} name={VegaIconNames.OPEN_EXTERNAL} />
</ExternalLink>
);
};
export const RewardsMovedNotification = () => {
const onRewardsPage = useMatch(Routes.REWARDS);
if (!onRewardsPage) return null;
return (
<NotificationBanner intent={Intent.Warning}>
<Trans
i18nKey="rewardsMovedNotification"
components={[<ConsoleRewardsLink>Console</ConsoleRewardsLink>]}
/>
</NotificationBanner>
);
};
@@ -10,6 +10,7 @@ import {
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { RewardsMovedNotification } from '../notifications/rewards-moved-notification';
interface AppLayoutProps {
children: ReactNode;
@@ -45,8 +46,10 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
const NotificationsContainer = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<div data-testid="banners">
<RewardsMovedNotification />
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
@@ -111,3 +111,4 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
</ContractsContext.Provider>
);
};
ContractsProvider.displayName = 'ContractsProvider';
@@ -288,7 +288,7 @@ describe('Consensus validators table', () => {
expect(
grid.querySelector('[role="gridcell"][col-id="totalPenalties"]')
).toHaveTextContent('13.16%');
).toHaveTextContent('10.07%');
expect(
grid.querySelector('[role="gridcell"][col-id="normalisedVotingPower"]')
@@ -185,15 +185,19 @@ export const ConsensusValidatorsTable = ({
const { rawValidatorScore: previousEpochValidatorScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakingPenalty = calculateOverallPenalty(
const overstakingPenalty = calculateOverstakedPenalty(
id,
allNodesInPreviousEpoch
);
const totalPenalty = calculateOverstakedPenalty(
const totalPenalty = calculateOverallPenalty(
id,
allNodesInPreviousEpoch
);
const lastEpochDataForNode = allNodesInPreviousEpoch.find(
(node) => node.id === id
);
return {
id,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
@@ -239,6 +243,12 @@ export const ConsensusValidatorsTable = ({
: undefined,
[ValidatorFields.MULTISIG_ERROR]:
multisigStatus?.showMultisigStatusError,
[ValidatorFields.MULTISIG_PENALTY]: formatNumberPercentage(
new BigNumber(1)
.minus(lastEpochDataForNode?.rewardScore?.multisigScore ?? 1)
.times(100),
2
),
};
}
);
@@ -378,7 +388,6 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.PENDING_STAKE,
@@ -400,6 +409,7 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.TOTAL_PENALTIES,
@@ -40,6 +40,7 @@ export enum ValidatorFields {
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
MULTISIG_ERROR = 'multisigError',
MULTISIG_PENALTY = 'multisigPenalty',
}
export const addUserDataToValidator = (
@@ -327,7 +328,7 @@ interface TotalPenaltiesRendererProps {
overstakedAmount: string;
overstakingPenalty: string;
totalPenalties: string;
multisigError?: boolean;
multisigPenalty: string;
};
}
@@ -346,11 +347,9 @@ export const TotalPenaltiesRenderer = ({
<div data-testid="overstaked-penalty-tooltip">
{t('overstakedPenalty')}: {data.overstakingPenalty}
</div>
{data.multisigError && (
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: 100%
</div>
)}
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: {data.multisigPenalty}
</div>
</>
}
>
@@ -37,7 +37,6 @@ import {
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
const statuses = {
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
@@ -105,9 +104,10 @@ export const ValidatorTable = ({
};
}, [node, previousEpochData?.epoch.validatorsConnection?.edges]);
const multisigStatus = previousEpochData
? getMultisigStatusInfo(previousEpochData)
: undefined;
const previousNodeData =
previousEpochData?.epoch.validatorsConnection?.edges?.find(
(e) => e?.node.id === node.id
);
return (
<>
@@ -293,21 +293,15 @@ export const ValidatorTable = ({
data-testid="multisig-penalty"
className="flex gap-2 items-baseline"
>
{multisigStatus?.zeroScoreNodes.find(
(n) => n.id === node.id
) ? (
<Tooltip
description={t('multisigPenaltyThisNodeIndicator')}
>
<span className="inline-block w-2 h-2 rounded-full bg-vega-red-500"></span>
</Tooltip>
) : null}
<Tooltip description={t('multisigPenaltyDescription')}>
<span>
{formatNumberPercentage(
BigNumber(
multisigStatus?.showMultisigStatusError ? 100 : 0
),
new BigNumber(1)
.minus(
previousNodeData?.node.rewardScore?.multisigScore ??
1
)
.times(100),
2
)}
</span>
-41
View File
@@ -1,41 +0,0 @@
NX_ETHEREUM_PROVIDER_URL=http://localhost:8545
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=''
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_ENV=CUSTOM
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_URL=http://localhost:3008/graphql
NX_VEGA_WALLET_URL=http://localhost:1789
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SENTRY_DSN=https://dummy@o999999.ingest.sentry.io/9999999
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
# Expose some env vars to cypress environment for market setup
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
CYPRESS_CONSOLE_URL=https://console.fairground.wtf
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
CYPRESS_ORACLE_PUBKEY=6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=02ecea…342f65
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=7f9cf0…c25535
CYPRESS_VEGA_ENV=CUSTOM
CYPRESS_VEGA_PUBLIC_KEY=02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65
CYPRESS_VEGA_PUBLIC_KEY2=7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535
CYPRESS_VEGA_TOKEN_URL=https://governance.fairground.wtf
CYPRESS_VEGA_URL=http://localhost:3008/graphql
CYPRESS_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_VEGA_WALLET_API_TOKEN=
# Cosmic elevator flags (MUST be doubled with CYPRESS_ prefix)
NX_SUCCESSOR_MARKETS=true
CYPRESS_NX_SUCCESSOR_MARKETS=true
-33
View File
@@ -1,33 +0,0 @@
NX_ETHEREUM_PROVIDER_URL=http://localhost:8545
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=''
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_ENV=CUSTOM
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_URL=http://localhost:3008/graphql
NX_VEGA_WALLET_URL=http://localhost:1789
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
# Expose some env vars to cypress environment for market setup
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
CYPRESS_CONSOLE_URL=https://console.fairground.wtf
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=02ecea…342f65
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=7f9cf0…c25535
CYPRESS_VEGA_ENV=CUSTOM
CYPRESS_VEGA_PUBLIC_KEY=02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65
CYPRESS_VEGA_PUBLIC_KEY2=7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535
CYPRESS_VEGA_TOKEN_URL=https://governance.fairground.wtf
CYPRESS_VEGA_URL=http://localhost:3008/graphql
CYPRESS_VEGA_WALLET_URL=http://localhost:1789
CYPRESS_VEGA_WALLET_API_TOKEN=
-19
View File
@@ -1,19 +0,0 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"cypress/unsafe-to-chain-command": 0
}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}
-41
View File
@@ -1,41 +0,0 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
reporter: '../../node_modules/cypress-mochawesome-reporter',
e2e: {
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('@cypress/grep/src/plugin')(config);
return config;
},
baseUrl: 'http://localhost:4200',
fileServerFolder: '.',
fixturesFolder: false,
specPattern: '**/*.cy.{js,jsx,ts,tsx}',
supportFile: './src/support/index.js',
video: false,
videosFolder: '../../dist/cypress/apps/trading-e2e/videos',
videoUploadOnPasses: false,
screenshotsFolder: '../../dist/cypress/apps/trading-e2e/screenshots',
chromeWebSecurity: false,
projectId: 'et4snf',
defaultCommandTimeout: 10000,
viewportWidth: 1800,
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
retries: 1,
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
ETHEREUM_CHAIN_ID: 11155111,
TRADING_MODE_LINK:
'https://docs.vega.xyz/testnet/concepts/trading-on-vega/trading-modes#auction-type-liquidity-monitoring',
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
grepOmitFiltered: true,
txTimeout: { timeout: 70000 },
},
});
-1
View File
@@ -1 +0,0 @@
declare module '*.scss';
-39
View File
@@ -1,39 +0,0 @@
{
"name": "trading-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/trading-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/trading-e2e/cypress.config.js",
"devServerTarget": "trading:serve"
},
"configurations": {
"production": {
"devServerTarget": "trading:serve:production"
},
"live": {
"devServerTarget": ""
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/trading-e2e/**/*.{js,ts}"]
}
},
"build": {
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/trading-e2e/"
}
}
},
"tags": [],
"implicitDependencies": ["trading"]
}
-20
View File
@@ -1,20 +0,0 @@
/// <reference types="cypress" />
declare namespace Cypress {
// specify additional properties in the TestConfig object
// in our case we will add "tags" property
interface SuiteConfigOverrides {
/**
* List of tags for this test
* @example a single tag
* it('logs in', { tags: '@smoke' }, () => { ... })
* @example multiple tags
* it('works', { tags: ['@smoke', '@slow'] }, () => { ... })
*/
tags?: string | string[];
}
interface Cypress {
grep?: (grep?: string, tags?: string, burn?: string) => void;
}
}
@@ -1,337 +0,0 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const txTimeout = Cypress.env('txTimeout');
const sepoliaUrl = Cypress.env('ETHERSCAN_URL');
const btcName = 0;
const vegaName = 4;
const btcSymbol = 'tBTC';
const vegaSymbol = 'VEGA';
const toastContent = 'toast-content';
const depositsTab = 'Deposits';
const toastCloseBtn = 'toast-close';
const completeWithdrawalBtn = 'complete-withdrawal';
const depositSubmit = 'deposit-submit';
const approveSubmit = 'approve-submit';
const dialogContent = 'dialog-content';
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
before(() => {
cy.createMarket();
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.visit('/#/portfolio');
});
it('can deposit', function () {
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
// 1001-DEPO-001
// 1001-DEPO-002
// 1001-DEPO-003
// 1001-DEPO-005
// 1001-DEPO-006
// 1001-DEPO-007
// 1001-DEPO-008
// 1001-DEPO-009
// 1001-DEPO-010
cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.getByTestId('approve-default').should(
'contain.text',
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
);
cy.getByTestId(approveSubmit).click();
cy.getByTestId('approve-pending').should('exist');
cy.getByTestId('approve-confirmed').should('exist');
cy.get(amountField).focus();
cy.get(amountField).clear().type('10');
cy.getByTestId(depositSubmit).click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
`Transaction confirmedYour transaction has been confirmed.View on EtherscanDeposit 10.00 ${btcSymbol}`,
{ matchCase: false }
);
cy.getByTestId(toastCloseBtn).click();
cy.getByTestId('Collateral').click();
cy.highlight('deposit verification');
cy.get('[col-id="asset.symbol"]', txTimeout).should(
'contain.text',
btcSymbol
);
cy.getByTestId(depositsTab).click();
cy.get('.ag-cell-value', txTimeout).should('contain.text', btcSymbol);
cy.get('[col-id="status"]').should('not.have.text', 'Open', txTimeout);
cy.get('[col-id="txHash"]')
.should('have.length.above', 2)
.eq(1)
.parent()
.within(() => {
cy.get('[col-id="asset.symbol"]').should('have.text', btcSymbol);
cy.get('[col-id="amount"]').should('have.text', '10.00');
cy.get('[col-id="createdTimestamp"]').should('not.be.empty');
cy.get('[col-id="status"]').should('have.text', 'Finalized');
cy.get('[col-id="txHash"]')
.find('a')
.should('have.attr', 'href')
.and('contain', `${sepoliaUrl}/tx/0x`);
});
});
it('can not withdrawal because of no MultiSign', function () {
// 1002-WITH-022
// 1002-WITH-023
// 0003-WTXN-011
cy.getByTestId('Withdrawals').click();
cy.getByTestId('withdraw-dialog-button').click();
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.get(amountField).focus();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
'Funds unlocked'
);
// cy.getByTestId(toastCloseBtn).click();
cy.highlight('withdrawals verification');
cy.getByTestId('toast-complete-withdrawal').last().click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
'Error occurredcannot estimate gas'
);
cy.getByTestId(completeWithdrawalBtn).should(
'contain.text',
'Complete withdrawal'
);
});
});
describe('capsule', { tags: '@slow', testIsolation: true }, () => {
before(() => {
cy.updateCapsuleMultiSig();
});
beforeEach(() => {
cy.createMarket();
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.setVegaWallet();
});
it('can withdrawal', function () {
// 1002-WITH-0014
// 1002-WITH-006
// 1002-WITH-009
// 1002-WITH-011
// 1002-WITH-024
// 1002-WITH-012
// 1002-WITH-013
// 1002-WITH-014
// 1002-WITH-015
// 1002-WITH-016
// 1002-WITH-017
// 1002-WITH-019
// 1002-WITH-020
// 1002-WITH-021
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId('Withdrawals').click();
cy.getByTestId('withdraw-dialog-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
'Funds unlocked'
);
cy.highlight('withdrawals verification');
cy.getByTestId('toast-complete-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
'Transaction confirmed'
);
cy.getByTestId(toastContent, txTimeout)
.should('contain.text', 'Funds unlocked')
.and('contain.text', 'Your funds have been unlocked for withdrawal.')
.and(
'contain.text',
'View in block explorerYou can save your withdrawal details for extra security.'
)
.and('contain.text', 'Withdraw 1.00 tBTCComplete withdrawal');
cy.getByTestId('toast-withdrawal-details').click();
cy.getByTestId(dialogContent)
.last()
.within(() => {
cy.getByTestId('dialog-title').should(
'contain.text',
'Save withdrawal details'
);
cy.getByTestId('copy-button').should('be.visible');
cy.getByTestId('assetSource_value').should(
'have.text',
'0xb63D135B0a6854EEb765d69ca36210cC70BECAE0'
);
cy.getByTestId('amount_value').should('have.text', '100000');
cy.getByTestId('nonce_value').invoke('text').should('not.be.empty');
cy.getByTestId('signatures_value')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('targetAddress_value').should(
'have.text',
ethWalletAddress
);
cy.getByTestId('creation_value').invoke('text').should('not.be.empty');
});
cy.getByTestId('close-withdrawal-approval-dialog').click();
cy.get('.ag-center-cols-container')
.find('[col-id="status"]')
.eq(0, txTimeout)
.should('contain.text', 'Completed');
cy.get('[col-id="txHash"]', txTimeout)
.should('have.length.above', 1)
.eq(1)
.parent()
.within(() => {
cy.get('[col-id="asset.symbol"]').should('have.text', btcSymbol);
cy.get('[col-id="amount"]').should('have.text', '1.00');
cy.get('[col-id="details.receiverAddress"]')
.find('a')
.should('have.attr', 'href')
.and('contain', `${sepoliaUrl}/address/`);
cy.get('[col-id="createdTimestamp"]').should('not.be.empty');
cy.get('[col-id="withdrawnTimestamp"]').should('not.be.empty');
cy.get('[col-id="status"]').should('have.text', 'Completed');
cy.get('[col-id="txHash"]')
.find('a')
.should('have.attr', 'href')
.and('contain', `${sepoliaUrl}/tx/0x`);
});
cy.getByTestId('withdraw-dialog-button').click({ force: true });
// cy.getByTestId('BALANCE_AVAILABLE_value').should('have.text', '6.999');
});
it('approved amount is less than deposit', function () {
// 1001-DEPO-006
// 1001-DEPO-007
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.contains('Deposits of tBTC not approved').should('not.exist');
cy.contains('Use maximum').should('be.visible');
cy.get(amountField).clear().type('20000000');
cy.getByTestId(depositSubmit).should('be.visible');
cy.getByTestId(depositSubmit).click();
cy.getByTestId('input-error-text').should(
'contain.text',
`You can't deposit more than you have in your Ethereum wallet`
);
});
it('withdraw - delay verification', function () {
// 1001-DEPO-024
// 1002-WITH-007
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]', txTimeout).should('exist');
cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(vegaName);
cy.getByTestId('approve-submit').click();
cy.getByTestId('approve-confirmed').should(
'contain.text',
'You approved deposits of up to VEGA'
);
cy.get(amountField).clear().type('10000');
cy.getByTestId('deposit-submit').click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
`Your transaction has been confirmed.`,
{ matchCase: false }
);
cy.getByTestId(toastCloseBtn).click({ multiple: true });
cy.getByTestId('Collateral').click();
cy.highlight('deposit verification');
cy.get('[col-id="asset.symbol"]', txTimeout).should(
'contain.text',
vegaSymbol
);
cy.getByTestId(depositsTab).click();
cy.get('.ag-cell-value', txTimeout).should('contain.text', vegaSymbol);
cy.get('[col-id="status"]').should('not.have.text', 'Open', txTimeout);
cy.get('[col-id="txHash"]')
.should('have.length.above', 2)
.eq(1)
.parent()
.within(() => {
cy.get('[col-id="asset.symbol"]').should('have.text', vegaSymbol);
cy.get('[col-id="amount"]').should('have.text', '10,000.00');
cy.get('[col-id="createdTimestamp"]').should('not.be.empty');
cy.get('[col-id="status"]').should('have.text', 'Finalized');
cy.get('[col-id="txHash"]')
.find('a')
.should('have.attr', 'href')
.and('contain', `${sepoliaUrl}/tx/0x`);
});
cy.getByTestId('Withdrawals').click(txTimeout);
cy.getByTestId('withdraw-dialog-button').click();
selectAsset(1);
cy.get(amountField).clear().type('10000');
cy.getByTestId('DELAY_TIME_value').should('have.text', '5 days');
cy.getByTestId('submit-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
'contain.text',
'Your funds have been unlocked'
);
cy.getByTestId(toastCloseBtn).click();
cy.getByTestId(completeWithdrawalBtn).first().should('be.visible').click();
cy.getByTestId(toastContent, txTimeout).should('contain.text', 'Delayed');
cy.getByTestId('tab-withdrawals').within(() => {
cy.get('.ag-center-cols-container')
.children()
.first()
.within(() => {
cy.get('[col-id="status"]').contains(
/Delayed \(ready in (\d{1,2}:\d{2}:\d{2}:\d{2})\)/
);
});
});
});
});
@@ -1,43 +0,0 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
const connectEthWalletBtn = 'connect-eth-wallet-btn';
describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
// Using portfolio withdrawals tab is it requires Ethereum wallet connection
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Withdrawals').click();
});
it('can connect', () => {
// 0004-EWAL-001
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-MetaMask').click();
cy.getByTestId('web3-connector-list').should('not.exist');
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it('able to disconnect eth wallet', () => {
// 0004-EWAL-004
// 0004-EWAL-005
// 0004-EWAL-006
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F');
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
cy.getByTestId(connectEthWalletBtn).should('exist');
});
});
@@ -1,29 +0,0 @@
import { OrderType } from '@vegaprotocol/types';
import type { OrderSubmission } from '@vegaprotocol/wallet';
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
export const createOrder = (order: OrderSubmission): void => {
cy.log('Placing order', order);
const { type, side, size, price, timeInForce, expiresAt } = order;
cy.getByTestId(
`order-type-${type === OrderType.TYPE_LIMIT ? 'Limit' : 'Market'}`
).click();
cy.getByTestId(`order-side-${side}`).click();
cy.getByTestId(orderSizeField).clear().type(size);
if (price) {
cy.getByTestId(orderPriceField).clear().type(price);
}
cy.getByTestId(orderTIFDropDown).select(timeInForce);
if (timeInForce === 'TIME_IN_FORCE_GTT') {
if (!expiresAt) {
throw new Error('Specify expiresAt if using GTT');
}
cy.getByTestId('date-picker-field').type(expiresAt);
}
cy.getByTestId(placeOrderBtn).click();
};
@@ -1,18 +0,0 @@
import * as Schema from '@vegaprotocol/types';
export const orderSizeField = 'order-size';
export const orderPriceField = 'order-price';
export const orderTIFDropDown = 'order-tif';
export const placeOrderBtn = 'place-order';
export const toggleShort = 'order-side-SIDE_SELL';
export const toggleLong = 'order-side-SIDE_BUY';
export const toggleLimit = 'order-type-Limit';
export const toggleMarket = 'order-type-Market';
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return {
code: Schema.OrderTimeInForceCode[value],
value,
text: Schema.OrderTimeInForceMapping[value],
};
});
@@ -1,5 +0,0 @@
export const connectEthereumWallet = (connectorName: string) => {
cy.getByTestId('connect-eth-wallet-btn').should('be.enabled').click();
cy.getByTestId('web3-connector-list').should('be.visible');
cy.getByTestId(`web3-connector-${connectorName}`).click();
};
-11
View File
@@ -1,11 +0,0 @@
export const selectAsset = (assetIndex: number) => {
cy.log(`selecting asset: ${assetIndex}`);
cy.getByTestId('select-asset').click();
cy.get('[data-testid="rich-select-option"]').eq(assetIndex).click();
// The asset only gets set once the queries (getWithdrawThreshold, getDelay)
// against the Ethereum change resolve, we should fix this but for now just force
// some wait time
// eslint-disable-next-line
cy.wait(100);
};
-8
View File
@@ -1,8 +0,0 @@
import '@vegaprotocol/cypress';
import 'cypress-real-events/support';
import registerCypressGrep from '@cypress/grep';
import { addMockTradingPage } from './trading';
import 'cypress-mochawesome-reporter/register';
registerCypressGrep();
addMockTradingPage();
@@ -1,36 +0,0 @@
import type {
OrdersUpdateSubscription,
OrdersUpdateSubscriptionVariables,
OrderUpdateFieldsFragment,
} from '@vegaprotocol/orders';
import type { onMessage } from '@vegaprotocol/cypress';
import type { PartialDeep } from 'type-fest';
import { orderUpdateSubscription } from '@vegaprotocol/mock';
const sendOrderUpdate: ((data: OrdersUpdateSubscription) => void)[] = [];
const getOnOrderUpdate = () => {
const onOrderUpdate: onMessage<
OrdersUpdateSubscription,
OrdersUpdateSubscriptionVariables
> = (send) => {
sendOrderUpdate.push(send);
};
return onOrderUpdate;
};
export const getSubscriptionMocks = () => ({
OrdersUpdate: getOnOrderUpdate(),
});
export function updateOrder(
override?: PartialDeep<OrderUpdateFieldsFragment>
): void {
const update: OrdersUpdateSubscription = orderUpdateSubscription({
// @ts-ignore partial deep check failing
orders: [override],
});
if (!sendOrderUpdate) {
throw new Error('OrderSub not called');
}
sendOrderUpdate.forEach((send) => send(update));
}
@@ -1,81 +0,0 @@
import type {
OrderAmendment,
OrderAmendmentBody,
OrderCancellation,
OrderCancellationBody,
OrderSubmission,
OrderSubmissionBody,
Transaction,
} from '@vegaprotocol/wallet';
export const testOrderSubmission = (
order: OrderSubmission,
expected?: Partial<OrderSubmission>
) => {
const expectedOrder = {
...order,
...expected,
};
const transaction: OrderSubmissionBody = {
orderSubmission: expectedOrder,
};
vegaWalletTransaction(transaction);
verifyToast();
};
export const testOrderAmendment = (
order: OrderAmendment,
expected?: Partial<OrderAmendment>
) => {
const expectedOrder = {
...order,
...expected,
};
const transaction: OrderAmendmentBody = {
orderAmendment: expectedOrder,
};
vegaWalletTransaction(transaction);
verifyToast();
};
export const testOrderCancellation = (
order: OrderCancellation,
expected?: Partial<OrderCancellation>
) => {
const expectedOrder = {
...order,
...expected,
};
const transaction: OrderCancellationBody = {
orderCancellation: expectedOrder,
};
vegaWalletTransaction(transaction);
verifyToast();
};
const vegaWalletTransaction = (transaction: Transaction) => {
cy.wait('@VegaWalletTransaction')
.its('request')
.then((req) => {
expect(req.body.params).to.deep.equal({
publicKey: Cypress.env('VEGA_PUBLIC_KEY'),
sendingMode: 'TYPE_SYNC',
transaction,
});
expect(req.headers.authorization).to.equal(
`VWT ${Cypress.env('VEGA_WALLET_API_TOKEN')}`
);
});
};
const verifyToast = () => {
cy.getByTestId('toast').should('contain.text', 'Awaiting confirmation');
cy.getByTestId('toast')
.find('a')
.invoke('attr', 'href')
.should('include', `${Cypress.env('EXPLORER_URL')}/txs/test-tx-hash`);
cy.getByTestId('toast-close').click();
};
-252
View File
@@ -1,252 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
import type { Provider, Status } from '@vegaprotocol/markets';
import {
accountsQuery,
assetQuery,
assetsQuery,
candlesQuery,
chartQuery,
depositsQuery,
estimateFeesQuery,
marginsQuery,
marketCandlesQuery,
marketDataQuery,
marketDepthQuery,
marketInfoQuery,
marketsCandlesQuery,
marketsDataQuery,
marketsQuery,
networkParamsQuery,
nodeGuardQuery,
ordersQuery,
estimatePositionQuery,
positionsQuery,
proposalListQuery,
tradesQuery,
withdrawalsQuery,
protocolUpgradeProposalsQuery,
blockStatisticsQuery,
networkParamQuery,
liquidityProvisionsQuery,
successorMarketQuery,
parentMarketIdQuery,
successorMarketIdsQuery,
successorMarketProposalDetailsQuery,
liquidityProvidersQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
type MarketPageMockData = {
state: Schema.MarketState;
tradingMode?: Schema.MarketTradingMode;
trigger?: Schema.AuctionTrigger;
};
const ORACLE_PUBKEY = Cypress.env('ORACLE_PUBKEY');
const marketDataOverride = (
data: MarketPageMockData
): PartialDeep<MarketDataQuery> => ({
marketsConnection: {
edges: [
{
node: {
data: {
// @ts-ignore conflict between incoming and outgoing types
trigger: data.trigger,
// @ts-ignore same as above
marketTradingMode: data.tradingMode,
marketState: data.state,
},
},
},
],
},
});
const marketsDataOverride = (
data: MarketPageMockData
): PartialDeep<MarketsQuery> => ({
marketsConnection: {
edges: [
{
node: {
// @ts-ignore conflict between incoming and outgoing types
tradingMode: data.tradingMode,
state: data.state,
},
},
],
},
});
const mockTradingPage = (
req: CyHttpMessages.IncomingHttpRequest,
state: Schema.MarketState = Schema.MarketState.STATE_ACTIVE,
tradingMode?: Schema.MarketTradingMode,
trigger?: Schema.AuctionTrigger
) => {
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(
req,
'Markets',
marketsQuery(marketsDataOverride({ state, tradingMode, trigger }))
);
aliasGQLQuery(
req,
'MarketData',
marketDataQuery(marketDataOverride({ state, tradingMode, trigger }))
);
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
aliasGQLQuery(req, 'MarketsCandles', marketsCandlesQuery());
aliasGQLQuery(req, 'MarketCandles', marketCandlesQuery());
aliasGQLQuery(req, 'MarketDepth', marketDepthQuery());
aliasGQLQuery(req, 'Orders', ordersQuery());
aliasGQLQuery(req, 'Accounts', accountsQuery());
aliasGQLQuery(req, 'Positions', positionsQuery());
aliasGQLQuery(req, 'Margins', marginsQuery());
aliasGQLQuery(req, 'Assets', assetsQuery());
aliasGQLQuery(req, 'Asset', assetQuery());
aliasGQLQuery(
req,
'MarketInfo',
marketInfoQuery({
market: {
tradableInstrument: {
instrument: {
product: {
__typename: 'Future',
dataSourceSpecForSettlementData: {
data: {
sourceType: {
sourceType: {
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: ORACLE_PUBKEY,
},
},
],
},
},
},
},
dataSourceSpecForTradingTermination: {
data: {
sourceType: {
sourceType: {
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: ORACLE_PUBKEY,
},
},
],
},
},
},
},
},
},
},
},
})
);
aliasGQLQuery(req, 'Trades', tradesQuery());
aliasGQLQuery(req, 'Chart', chartQuery());
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
aliasGQLQuery(req, 'LiquidityProviders', liquidityProvidersQuery());
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(req, 'NetworkParam', networkParamQuery);
aliasGQLQuery(req, 'EstimateFees', estimateFeesQuery());
aliasGQLQuery(req, 'EstimatePosition', estimatePositionQuery());
aliasGQLQuery(req, 'ProposalsList', proposalListQuery());
aliasGQLQuery(req, 'Deposits', depositsQuery());
aliasGQLQuery(
req,
'ProtocolUpgradeProposals',
protocolUpgradeProposalsQuery()
);
aliasGQLQuery(req, 'BlockStatistics', blockStatisticsQuery());
aliasGQLQuery(req, 'SuccessorMarket', successorMarketQuery());
aliasGQLQuery(req, 'ParentMarketId', parentMarketIdQuery());
aliasGQLQuery(req, 'SuccessorMarketIds', successorMarketIdsQuery());
aliasGQLQuery(
req,
'SuccessorMarketProposalDetails',
successorMarketProposalDetailsQuery()
);
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
mockTradingPage(
state?: Schema.MarketState,
tradingMode?: Schema.MarketTradingMode,
trigger?: Schema.AuctionTrigger,
oracleStatus?: Status
): void;
}
}
}
export const addMockTradingPage = () => {
Cypress.Commands.add(
'mockTradingPage',
(
state = Schema.MarketState.STATE_ACTIVE,
tradingMode,
trigger,
oracleStatus
) => {
cy.mockChainId();
cy.mockGQL((req) => {
mockTradingPage(req, state, tradingMode, trigger);
});
const oracle: Provider = {
name: 'Another oracle',
url: 'https://zombo.com',
description_markdown:
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
oracle: {
status: oracleStatus || 'GOOD',
status_reason: '',
first_verified: '2022-01-01T00:00:00.000Z',
last_verified: '2022-12-31T00:00:00.000Z',
type: 'public_key',
public_key: ORACLE_PUBKEY,
},
proofs: [
{
format: 'signed_message',
available: true,
type: 'public_key',
public_key: ORACLE_PUBKEY,
message: 'SOMEHEX',
},
],
github_link: `https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/public_key-${ORACLE_PUBKEY}.toml`,
};
// Prevent request to github, return some dummy content
cy.intercept(
'GET',
/^https:\/\/raw.githubusercontent.com\/vegaprotocol\/well-known/,
{
body: [oracle],
}
);
}
);
};
-19
View File
@@ -1,19 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node", "cypress-real-events", "@cypress/grep"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.js", "./declaration.d.ts"]
}
+1 -7
View File
@@ -49,10 +49,4 @@ To run the minimal set of unit tests, run the following:
yarn nx test trading
```
To run the UI automation tests with a mocked API, run:
```bash
yarn nx run trading-e2e:e2e
```
To run tests with market sim please read [the readme](e2e/README.md).
To run the UI automation tests please read [e2e/README.md](e2e/README.md)
@@ -0,0 +1 @@
export { ProfileDialog } from './profile-dialog';
@@ -0,0 +1,150 @@
import {
Dialog,
FormGroup,
Input,
InputError,
Intent,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useProfileDialogStore } from '../../stores/profile-dialog-store';
import { useForm } from 'react-hook-form';
import { useT } from '../../lib/use-t';
import { useRequired } from '@vegaprotocol/utils';
import {
useSimpleTransaction,
type Status,
useVegaWallet,
} from '@vegaprotocol/wallet-react';
import {
usePartyProfilesQuery,
type PartyProfilesQuery,
} from '../vega-wallet-connect-button/__generated__/PartyProfiles';
export const ProfileDialog = () => {
const t = useT();
const { pubKeys } = useVegaWallet();
const { data, refetch } = usePartyProfilesQuery({
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
skip: pubKeys.length <= 0,
});
const open = useProfileDialogStore((store) => store.open);
const pubKey = useProfileDialogStore((store) => store.pubKey);
const setOpen = useProfileDialogStore((store) => store.setOpen);
const { send, status, error, reset } = useSimpleTransaction({
onSuccess: () => {
refetch();
},
});
const profileEdge = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pubKey
);
const sendTx = (field: FormFields) => {
send({
updatePartyProfile: {
alias: field.alias,
metadata: [],
},
});
};
return (
<Dialog
open={open}
onChange={() => {
setOpen(undefined);
reset();
}}
title={t('Edit profile')}
>
<ProfileForm
profile={profileEdge?.node}
status={status}
error={error}
onSubmit={sendTx}
/>
</Dialog>
);
};
interface FormFields {
alias: string;
}
type Profile = NonNullable<
PartyProfilesQuery['partiesProfilesConnection']
>['edges'][number]['node'];
const ProfileForm = ({
profile,
onSubmit,
status,
error,
}: {
profile: Profile | undefined;
onSubmit: (fields: FormFields) => void;
status: Status;
error: string | undefined;
}) => {
const t = useT();
const required = useRequired();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
alias: profile?.alias,
},
});
const renderButtonText = () => {
if (status === 'requested') {
return t('Confirm in wallet...');
}
if (status === 'pending') {
return t('Confirming transaction...');
}
return t('Submit');
};
const errorMessage = errors.alias?.message || error;
return (
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
<FormGroup label="Alias" labelFor="alias">
<Input
{...register('alias', {
validate: {
required,
},
})}
/>
{errorMessage && (
<InputError>
<p className="break-words max-w-full first-letter:uppercase">
{errorMessage}
</p>
</InputError>
)}
{status === 'confirmed' && (
<p className="mt-2 mb-4 text-sm text-success">
{t('Profile updated')}
</p>
)}
</FormGroup>
<TradingButton
type="submit"
intent={Intent.Info}
disabled={status === 'requested' || status === 'pending'}
>
{renderButtonText()}
</TradingButton>
</form>
);
};
@@ -0,0 +1,14 @@
query PartyProfiles($partyIds: [ID!]) {
partiesProfilesConnection(ids: $partyIds) {
edges {
node {
partyId
alias
metadata {
key
value
}
}
}
}
}
@@ -0,0 +1,57 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PartyProfilesQueryVariables = Types.Exact<{
partyIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
export type PartyProfilesQuery = { __typename?: 'Query', partiesProfilesConnection?: { __typename?: 'PartiesProfilesConnection', edges: Array<{ __typename?: 'PartyProfileEdge', node: { __typename?: 'PartyProfile', partyId: string, alias: string, metadata: Array<{ __typename?: 'Metadata', key: string, value: string }> } }> } | null };
export const PartyProfilesDocument = gql`
query PartyProfiles($partyIds: [ID!]) {
partiesProfilesConnection(ids: $partyIds) {
edges {
node {
partyId
alias
metadata {
key
value
}
}
}
}
}
`;
/**
* __usePartyProfilesQuery__
*
* To run a query within a React component, call `usePartyProfilesQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyProfilesQuery` 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 query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = usePartyProfilesQuery({
* variables: {
* partyIds: // value for 'partyIds'
* },
* });
*/
export function usePartyProfilesQuery(baseOptions?: Apollo.QueryHookOptions<PartyProfilesQuery, PartyProfilesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyProfilesQuery, PartyProfilesQueryVariables>(PartyProfilesDocument, options);
}
export function usePartyProfilesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyProfilesQuery, PartyProfilesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyProfilesQuery, PartyProfilesQueryVariables>(PartyProfilesDocument, options);
}
export type PartyProfilesQueryHookResult = ReturnType<typeof usePartyProfilesQuery>;
export type PartyProfilesLazyQueryHookResult = ReturnType<typeof usePartyProfilesLazyQuery>;
export type PartyProfilesQueryResult = Apollo.QueryResult<PartyProfilesQuery, PartyProfilesQueryVariables>;
@@ -1,21 +1,57 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { VegaWalletConnectButton } from './vega-wallet-connect-button';
import { truncateByChars } from '@vegaprotocol/utils';
import userEvent from '@testing-library/user-event';
import {
mockConfig,
MockedWalletProvider,
} from '@vegaprotocol/wallet-react/testing';
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import {
PartyProfilesDocument,
type PartyProfilesQuery,
} from './__generated__/PartyProfiles';
jest.mock('../../lib/hooks/use-get-current-route-id', () => ({
useGetCurrentRouteId: jest.fn().mockReturnValue('current-route-id'),
}));
const key = { publicKey: '123456__123456', name: 'test' };
const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' };
const keys = [key, key2];
const keyProfile = {
__typename: 'PartyProfile' as const,
partyId: key.publicKey,
alias: `${key.name} alias`,
metadata: [],
};
const renderComponent = (mockOnClick = jest.fn()) => {
const partyProfilesMock: MockedResponse<PartyProfilesQuery> = {
request: {
query: PartyProfilesDocument,
variables: { partyIds: keys.map((k) => k.publicKey) },
},
result: {
data: {
partiesProfilesConnection: {
__typename: 'PartiesProfilesConnection',
edges: [
{
__typename: 'PartyProfileEdge',
node: keyProfile,
},
],
},
},
},
};
return (
<MockedWalletProvider>
<VegaWalletConnectButton onClick={mockOnClick} />
</MockedWalletProvider>
<MockedProvider mocks={[partyProfilesMock]}>
<MockedWalletProvider>
<VegaWalletConnectButton onClick={mockOnClick} />
</MockedWalletProvider>
</MockedProvider>
);
};
@@ -43,10 +79,6 @@ describe('VegaWalletConnectButton', () => {
});
it('should open dropdown and refresh keys when connected', async () => {
const key = { publicKey: '123456__123456', name: 'test' };
const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' };
const keys = [key, key2];
mockConfig.store.setState({
status: 'connected',
keys,
@@ -61,14 +93,22 @@ describe('VegaWalletConnectButton', () => {
expect(screen.queryByTestId('connect-vega-wallet')).not.toBeInTheDocument();
const button = screen.getByTestId('manage-vega-wallet');
expect(button).toHaveTextContent(truncateByChars(key.publicKey));
expect(button).toHaveTextContent(key.name);
fireEvent.click(button);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(await screen.findAllByRole('menuitemradio')).toHaveLength(
keys.length
const menuItems = await screen.findAllByRole('menuitemradio');
expect(menuItems).toHaveLength(keys.length);
expect(within(menuItems[0]).getByTestId('alias')).toHaveTextContent(
keyProfile.alias
);
expect(within(menuItems[1]).getByTestId('alias')).toHaveTextContent(
'No alias'
);
expect(refreshKeys).toHaveBeenCalled();
fireEvent.click(screen.getByTestId(`key-${key2.publicKey}`));
@@ -14,6 +14,7 @@ import {
TradingDropdownItem,
TradingDropdownRadioItem,
TradingDropdownItemIndicator,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isBrowserWalletInstalled, type Key } from '@vegaprotocol/wallet';
import { useDialogStore, useVegaWallet } from '@vegaprotocol/wallet-react';
@@ -22,6 +23,8 @@ import classNames from 'classnames';
import { ViewType, useSidebar } from '../sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { usePartyProfilesQuery } from './__generated__/PartyProfiles';
import { useProfileDialogStore } from '../../stores/profile-dialog-store';
export const VegaWalletConnectButton = ({
intent = Intent.None,
@@ -68,10 +71,10 @@ export const VegaWalletConnectButton = ({
{activeKey ? (
<>
{activeKey && (
<span className="uppercase">{activeKey.name}</span>
<span className="uppercase">
{activeKey.name ? activeKey.name : t('Unnamed key')}
</span>
)}
{' | '}
{truncateByChars(activeKey.publicKey)}
</>
) : (
<>{'Select key'}</>
@@ -88,20 +91,11 @@ export const VegaWalletConnectButton = ({
onEscapeKeyDown={() => setDropdownOpen(false)}
>
<div className="min-w-[340px]" data-testid="keypair-list">
<TradingDropdownRadioGroup
value={pubKey || undefined}
onValueChange={(value) => {
selectPubKey(value);
}}
>
{pubKeys.map((pk) => (
<KeypairItem
key={pk.publicKey}
pk={pk}
active={pk.publicKey === pubKey}
/>
))}
</TradingDropdownRadioGroup>
<KeypairRadioGroup
pubKey={pubKey}
pubKeys={pubKeys}
onSelect={selectPubKey}
/>
<TradingDropdownSeparator />
{!isReadOnly && (
<TradingDropdownItem
@@ -141,28 +135,52 @@ export const VegaWalletConnectButton = ({
);
};
const KeypairItem = ({ pk, active }: { pk: Key; active: boolean }) => {
const KeypairRadioGroup = ({
pubKey,
pubKeys,
onSelect,
}: {
pubKey: string | undefined;
pubKeys: Key[];
onSelect: (pubKey: string) => void;
}) => {
const { data } = usePartyProfilesQuery({
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
skip: pubKeys.length <= 0,
});
return (
<TradingDropdownRadioGroup value={pubKey} onValueChange={onSelect}>
{pubKeys.map((pk) => {
const profile = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pk.publicKey
);
return (
<KeypairItem key={pk.publicKey} pk={pk} alias={profile?.node.alias} />
);
})}
</TradingDropdownRadioGroup>
);
};
const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => {
const t = useT();
const [copied, setCopied] = useCopyTimeout();
const setOpen = useProfileDialogStore((store) => store.setOpen);
return (
<TradingDropdownRadioItem value={pk.publicKey}>
<div
className={classNames('flex-1 mr-2', {
'text-default': active,
'text-muted': !active,
})}
data-testid={`key-${pk.publicKey}`}
>
<span className={classNames('mr-2 uppercase')}>
{pk.name}
<div>
<div className="flex items-center gap-2">
<span>{pk.name ? pk.name : t('Unnamed key')}</span>
{' | '}
{truncateByChars(pk.publicKey)}
</span>
<span className="inline-flex items-center gap-1">
<span className="font-mono">
{truncateByChars(pk.publicKey, 3, 3)}
</span>
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
<button
data-testid="copy-vega-public-key"
className="relative -top-px"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
@@ -170,7 +188,17 @@ const KeypairItem = ({ pk, active }: { pk: Key; active: boolean }) => {
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
</span>
</div>
<div
className={classNames('flex-1 mr-2 text-secondary text-sm')}
data-testid={`key-${pk.publicKey}`}
>
<Tooltip description={t('Public facing key alias. Click to edit')}>
<button data-testid="alias" onClick={() => setOpen(pk.publicKey)}>
{alias ? alias : t('No alias')}
</button>
</Tooltip>
</div>
</div>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.6
VEGA_VERSION=v0.75.0-preview.2
LOCAL_SERVER=false
-2
View File
@@ -50,8 +50,6 @@ def truncate_middle(market_id, start=6, end=4):
def change_keys(page: Page, vega: VegaServiceNull, key_name):
page.get_by_test_id("manage-vega-wallet").click()
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
page.click(
f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
page.reload()
+4 -4
View File
@@ -877,13 +877,13 @@ testing = ["filelock"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
@@ -1166,7 +1166,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "HEAD"
resolved_reference = "33fec45ce8044ef7f53b625584ce590d174f9057"
resolved_reference = "53eed8942acb670783105cb1115bab76710a46dc"
[[package]]
name = "websocket-client"
@@ -77,7 +77,7 @@ def test_market_info_market_volume(page: Page):
page.get_by_test_id(market_title_test_id).get_by_text(
"Market volume").click()
fields = [
["24 Hour Volume", "-"],
["24 Hour Volume", "0 (0 )"],
["Open Interest", "1"],
["Best Bid Volume", "99"],
["Best Offer Volume", "99"],
@@ -57,7 +57,7 @@ class TestPerpetuals:
page.goto(f"/#/markets/{perps_market}")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("9.00 tDAI")
expect(row.locator(col_amount)).to_have_text("4.45 tDAI")
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_funding_payment_loss(self, perps_market, page: Page, vega):
@@ -65,7 +65,7 @@ class TestPerpetuals:
change_keys(page, vega, "market_maker")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("-27.00 tDAI")
expect(row.locator(col_amount)).to_have_text("-13.35 tDAI")
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_funding_header(self, perps_market, page: Page):
+6 -2
View File
@@ -25,8 +25,12 @@ fragment GameFields on Game {
}
}
query Games($epochFrom: Int) {
games(epochFrom: $epochFrom, entityScope: ENTITY_SCOPE_TEAMS) {
query Games($epochFrom: Int, $teamId: ID) {
games(
epochFrom: $epochFrom
teamId: $teamId
entityScope: ENTITY_SCOPE_TEAMS
) {
edges {
node {
...GameFields
+4 -2
View File
@@ -9,6 +9,7 @@ export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: numbe
export type GamesQueryVariables = Types.Exact<{
epochFrom?: Types.InputMaybe<Types.Scalars['Int']>;
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
@@ -44,8 +45,8 @@ export const GameFieldsFragmentDoc = gql`
}
${TeamEntityFragmentDoc}`;
export const GamesDocument = gql`
query Games($epochFrom: Int) {
games(epochFrom: $epochFrom, entityScope: ENTITY_SCOPE_TEAMS) {
query Games($epochFrom: Int, $teamId: ID) {
games(epochFrom: $epochFrom, teamId: $teamId, entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...GameFields
@@ -68,6 +69,7 @@ export const GamesDocument = gql`
* const { data, loading, error } = useGamesQuery({
* variables: {
* epochFrom: // value for 'epochFrom'
* teamId: // value for 'teamId'
* },
* });
*/
+1
View File
@@ -51,6 +51,7 @@ export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
const { data, loading, error } = useGamesQuery({
variables: {
epochFrom: from,
teamId: teamId,
},
skip: !from,
fetchPolicy: 'cache-and-network',
-2
View File
@@ -24,7 +24,6 @@ import {
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
import { Telemetry } from '../components/telemetry';
import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
@@ -77,7 +76,6 @@ function AppBody({ Component }: AppProps) {
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
<ViewingBanner />
</div>
<div data-testid={`pathname-${location.pathname}`}>
<Component />
+2
View File
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/web3';
import { WelcomeDialog } from '../components/welcome-dialog';
import { VegaWalletConnectDialog } from '../components/vega-wallet-connect-dialog';
import { ProfileDialog } from '../components/profile-dialog';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
@@ -24,6 +25,7 @@ const DialogsContainer = () => {
<WelcomeDialog />
<Web3ConnectUncontrolledDialog />
<WithdrawalApprovalDialogContainer />
<ProfileDialog />
</>
);
};
@@ -0,0 +1,19 @@
import { create } from 'zustand';
interface ProfileDialogStore {
open: boolean;
pubKey: string | undefined;
setOpen: (pubKey: string | undefined) => void;
}
export const useProfileDialogStore = create<ProfileDialogStore>((set) => ({
open: false,
pubKey: undefined,
setOpen: (pubKey) => {
if (pubKey) {
set({ open: true, pubKey });
} else {
set({ open: false, pubKey: undefined });
}
},
}));
@@ -12,7 +12,7 @@ declare global {
}
}
const chainId = 'test-id';
const chainId = 'vega-stagnet1-202307191148';
export function addMockChainId() {
Cypress.Commands.add('mockChainId', () => {
@@ -37,10 +37,12 @@ export function addVegaWalletConnect() {
Cypress.Commands.add('connectVegaWallet', (isMobile) => {
mockConnectWallet();
cy.highlight(`Connecting Vega Wallet`);
cy.get('[data-testid="splash-loader"]', { timeout: 20000 }).should(
'not.exist'
);
const connectVegaWalletButton = `[data-testid=connect-vega-wallet${
isMobile ? '-mobile' : ''
}]:visible`;
cy.get(connectVegaWalletButton).then((btn) => {
if (btn.length === 0) {
cy.log('could not find the button, perhaps already connected');
+3 -2
View File
@@ -13,7 +13,8 @@ declare global {
const hasMethod = (req: CyHttpMessages.IncomingHttpRequest, method: string) => {
const { body } = req;
return 'method' in body && body.method === method;
const b = JSON.parse(body);
return 'method' in b && b.method === method;
};
export function addMockWalletCommand() {
@@ -72,7 +73,7 @@ export const aliasWalletConnectQuery = (
body: {
jsonrpc: '2.0',
result: {
chainID: 'test-id',
chainID: 'vega-fairground-202305051805',
},
id: '1',
},
+1
View File
@@ -134,6 +134,7 @@ export const CONSOLE_TRANSFER = '#/portfolio/assets/transfer';
export const CONSOLE_TRANSFER_ASSET =
'#/portfolio/assets/transfer?assetId=:assetId';
export const CONSOLE_MARKET_PAGE = '#/markets/:marketId';
export const CONSOLE_REWARDS_PAGE = '#/rewards';
// Governance pages
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
+2 -1
View File
@@ -971,5 +971,6 @@
"YourIdentityAnonymous": "Your identity is always anonymous on Vega",
"yourStake": "Your stake",
"yourVote": "Your vote",
"youVoted": "You voted"
"youVoted": "You voted",
"rewardsMovedNotification": "Trading and liquidity rewards have moved. Visit <0>Console</0> to view your rewards."
}
-5
View File
@@ -1,8 +1,6 @@
{
"{{liquidityPriceRange}} of mid price": "{{liquidityPriceRange}} of mid price",
"{{probability}} probability price bounds": "{{probability}} probability price bounds",
"24 hour change is unavailable at this time. The price change in the last 120 hours is:": "24 hour change is unavailable at this time. The price change in the last 120 hours is:",
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"A concept derived from traditional markets. It is a calculated value for the current market price on a market.": "A concept derived from traditional markets. It is a calculated value for the current market price on a market.",
"A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.": "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.",
"A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.": "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.",
@@ -51,9 +49,6 @@
"Market": "Market",
"Market data": "Market data",
"Market governance": "Market governance",
"Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"Market ID": "Market ID",
"Market price": "Market price",
"Market specification": "Market specification",
+10
View File
@@ -86,6 +86,7 @@
"Docs": "Docs",
"Earn commission & stake rewards": "Earn commission & stake rewards",
"Earned by me": "Earned by me",
"Edit alias": "Edit alias",
"Eligible teams": "Eligible teams",
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
"[empty]": "[empty]",
@@ -162,6 +163,7 @@
"Joined": "Joined",
"Joined at": "Joined at",
"Joined epoch": "Joined epoch",
"Key name": "Key name",
"gameCount_one": "Last game result",
"gameCount_other": "Last {{count}} game results",
"Learn about providing liquidity": "Learn about providing liquidity",
@@ -194,6 +196,7 @@
"My liquidity provision": "My liquidity provision",
"My trading fees": "My trading fees",
"Name": "Name",
"No alias": "No alias",
"No closed orders": "No closed orders",
"No data": "No data",
"No deposits": "No deposits",
@@ -227,6 +230,7 @@
"Not connected": "Not connected",
"Number of epochs after distribution to delay vesting of rewards by": "Number of epochs after distribution to delay vesting of rewards by",
"Number of traders": "Number of traders",
"On-change alias": "On-change alias",
"Open": "Open",
"Open a position": "Open a position",
"Open markets": "Open markets",
@@ -249,6 +253,7 @@
"Portfolio": "Portfolio",
"Positions": "Positions",
"Price": "Price",
"Profile updated": "Profile updated",
"Program ends:": "Program ends:",
"Propose a new market": "Propose a new market",
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
@@ -289,6 +294,7 @@
"Search": "Search",
"See all markets": "See all markets",
"Select market": "Select market",
"Set party alias": "Set party alias",
"Settings": "Settings",
"Settlement asset": "Settlement asset",
"Settlement date": "Settlement date",
@@ -311,6 +317,7 @@
"Stop": "Stop",
"Stop orders": "Stop orders",
"Streak reward multiplier": "Streak reward multiplier",
"Submit": "Submit",
"Successor of a market": "Successor of a market",
"Successors to this market have been proposed": "Successors to this market have been proposed",
"Supplied stake": "Supplied stake",
@@ -371,6 +378,7 @@
"Staking rewards": "Staking rewards",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Unnamed key": "Unnamed key",
"Update team": "Update team",
"URL": "URL",
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
@@ -407,8 +415,10 @@
"You will no longer be able to hold a position on this market when it closes in {{duration}}.": "You will no longer be able to hold a position on this market when it closes in {{duration}}.",
"Your code has been rejected": "Your code has been rejected",
"Your identity is always anonymous on Vega": "Your identity is always anonymous on Vega",
"Your key's private name, can be changed in your wallet": "Your key's private name, can be changed in your wallet",
"Your referral code": "Your referral code",
"Your tier": "Your tier",
"Your public alias, stored on chain": "Your public alias, stored on chain",
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
@@ -7,6 +7,7 @@
"Get MetaMask": "Get MetaMask",
"Get the Vega Wallet": "Get the Vega Wallet",
"I agree": "I agree",
"Once you have the added the extension, <0>refresh</0> you browser.": "Once you have the added the extension, <0>refresh</0> you browser.",
"Successfully connected": "Successfully connected",
"Transaction was not successful": "Transaction was not successful",
"Wallet rejected transaction": "Wallet rejected transaction"
@@ -2,16 +2,14 @@ import { type ReactNode } from 'react';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
getDateTimeFormat,
priceChange,
priceChangePercentage,
} from '@vegaprotocol/utils';
import { PriceChangeCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { signedNumberCssClass } from '@vegaprotocol/datagrid';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { useCandles } from '../../hooks/use-candles';
import BigNumber from 'bignumber.js';
import classNames from 'classnames';
import { useT } from '../../use-t';
interface Props {
marketId?: string;
@@ -24,7 +22,6 @@ export const Last24hPriceChange = ({
decimalPlaces,
fallback,
}: Props) => {
const t = useT();
const { oneDayCandles, fiveDaysCandles, error } = useCandles({
marketId,
});
@@ -35,56 +32,6 @@ export const Last24hPriceChange = ({
return nonIdeal;
}
if (fiveDaysCandles.length < 24) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
}
)}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'24 hour change is unavailable at this time. The price change in the last 120 hours is:'
)}{' '}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
const candles = oneDayCandles?.map((c) => c.close) || [];
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
@@ -2,7 +2,6 @@ import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
import {
addDecimalsFormatNumber,
formatNumber,
getDateTimeFormat,
isNumeric,
} from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
@@ -37,83 +36,6 @@ export const Last24hVolume = ({
return nonIdeal;
}
if (fiveDaysCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
marketDecimals,
positionDecimalPlaces
);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
candleVolumeValue,
candleVolumePrice,
quoteUnit,
}
)}
</span>
</div>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
marketDecimals,
positionDecimalPlaces
);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of ({{candleVolumePrice}} {{quoteUnit}})',
{ candleVolumeValue, candleVolumePrice, quoteUnit }
)}
</span>
</div>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
const candleVolume = oneDayCandles
? calcCandleVolume(oneDayCandles)
: initialValue;
+1 -8
View File
@@ -8,7 +8,7 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
const fiveDaysAgo = useFiveDaysAgo();
const yesterday = useYesterday();
const since = new Date(fiveDaysAgo).toISOString();
const { data, error } = useThrottledDataProvider({
const { data: fiveDaysCandles, error } = useThrottledDataProvider({
dataProvider: marketCandlesProvider,
variables: {
marketId: marketId || '',
@@ -18,13 +18,6 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
skip: !marketId,
});
const fiveDaysCandles = data?.filter((c) => {
if (c.open === '' || c.close === '' || c.high === '' || c.close === '') {
return false;
}
return true;
});
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
+12
View File
@@ -936,6 +936,8 @@ export enum DispatchMetric {
/** Dispatch strategy for a recurring transfer */
export type DispatchStrategy = {
__typename?: 'DispatchStrategy';
/** Optional multiplier on taker fees used to cap the rewards a party may receive in an epoch */
capRewardFeeMultiple?: Maybe<Scalars['String']>;
/** Defines the data that will be used to compare markets so as to distribute rewards appropriately */
dispatchMetric: DispatchMetric;
/** The asset to use for measuring contribution to the metric */
@@ -2391,6 +2393,8 @@ export type Market = {
state: MarketState;
/** Optional: Market ID of the successor to this market if one exists */
successorMarketID?: Maybe<Scalars['ID']>;
/** The market minimum tick size */
tickSize: Scalars['String'];
/** An instance of, or reference to, a tradable instrument. */
tradableInstrument: TradableInstrument;
/** @deprecated Simplify and consolidate trades query and remove nesting. Use trades query instead */
@@ -2816,6 +2820,8 @@ export type NewMarket = {
riskParameters: RiskModel;
/** Successor market configuration. If this proposed market is meant to succeed a given market, then this needs to be set. */
successorConfiguration?: Maybe<SuccessorConfiguration>;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
/** Configuration for a new spot market on Vega */
@@ -2839,6 +2845,8 @@ export type NewSpotMarket = {
riskParameters?: Maybe<RiskModel>;
/** Specifies parameters related to liquidity target stake calculation */
targetStakeParameters: TargetStakeParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type NewTransfer = {
@@ -7074,6 +7082,8 @@ export type UpdateMarketConfiguration = {
quadraticSlippageFactor: Scalars['String'];
/** Updated futures market risk model parameters. */
riskParameters: UpdateMarketRiskParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type UpdateMarketLogNormalRiskModel = {
@@ -7171,6 +7181,8 @@ export type UpdateSpotMarketConfiguration = {
riskParameters: RiskModel;
/** Specifies parameters related to target stake calculation */
targetStakeParameters: TargetStakeParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type UpdateVolumeDiscountProgram = {
@@ -17,8 +17,8 @@ export const InputError = ({
...props
}: InputErrorProps) => {
const effectiveClassName = classNames(
'text-sm flex items-center first-letter:uppercase',
'mt-2',
'text-sm block items-center first-letter:capitalize',
'mt-2 min-w-0 break-words',
{
'border-danger': intent === 'danger',
'border-warning': intent === 'warning',
@@ -1,4 +1,9 @@
import { type ReactNode, type FunctionComponent, forwardRef } from 'react';
import {
type ReactNode,
type FunctionComponent,
forwardRef,
useState,
} from 'react';
import {
ConnectorErrors,
isBrowserWalletInstalled,
@@ -12,6 +17,7 @@ import { useConnect } from '../../hooks/use-connect';
import { Links } from '../../constants';
import { ConnectorIcon } from './connector-icon';
import { useUserAgent } from '@vegaprotocol/react-helpers';
import { Trans } from 'react-i18next';
const vegaExtensionsLinks = {
chrome: Links.chromeExtension,
@@ -29,48 +35,67 @@ export const ConnectionOptions = ({
onConnect: (id: ConnectorType) => void;
}) => {
const t = useT();
const error = useWallet((store) => store.error);
const { connectors } = useConnect();
const error = useWallet((store) => store.error);
const [isInstalling, setIsInstalling] = useState(false);
return (
<div className="flex flex-col items-start gap-4">
<h2 className="text-xl">{t('Connect to Vega')}</h2>
<ul
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
data-testid="connectors-list"
>
{connectors.map((c) => {
const ConnectionOption = ConnectionOptionRecord[c.id];
const props = {
id: c.id,
name: c.name,
description: c.description,
showDescription: false,
onClick: () => onConnect(c.id),
};
if (ConnectionOption) {
return (
<li key={c.id}>
<ConnectionOption {...props} />
</li>
);
}
return (
<li key={c.id}>
<ConnectionOptionDefault {...props} />
</li>
);
})}
</ul>
{error && error.code !== ConnectorErrors.userRejected.code && (
<p
className="text-danger text-sm first-letter:uppercase"
data-testid="connection-error"
>
{error.message}
{isInstalling ? (
<p className="text-warning">
<Trans
i18nKey="Once you have the added the extension, <0>refresh</0> you browser."
components={[
<button
onClick={() => window.location.reload()}
className="underline underline-offset-4"
/>,
]}
/>
</p>
) : (
<>
<ul
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
data-testid="connectors-list"
>
{connectors.map((c) => {
const ConnectionOption = ConnectionOptionRecord[c.id];
const props = {
id: c.id,
name: c.name,
description: c.description,
showDescription: false,
onClick: () => onConnect(c.id),
onInstall: () => setIsInstalling(true),
};
if (ConnectionOption) {
return (
<li key={c.id}>
<ConnectionOption {...props} />
</li>
);
}
return (
<li key={c.id}>
<ConnectionOptionDefault {...props} />
</li>
);
})}
</ul>
{error && error.code !== ConnectorErrors.userRejected.code && (
<p
className="text-danger text-sm first-letter:uppercase"
data-testid="connection-error"
>
{error.message}
{error.data ? `: ${error.data}` : ''}
</p>
)}
</>
)}
<a
href={Links.walletOverview}
@@ -90,6 +115,7 @@ interface ConnectionOptionProps {
description: string;
showDescription?: boolean;
onClick: () => void;
onInstall?: () => void;
}
const CONNECTION_OPTION_CLASSES =
@@ -142,6 +168,7 @@ export const ConnectionOptionInjected = ({
description,
showDescription = false,
onClick,
onInstall,
}: ConnectionOptionProps) => {
const t = useT();
const userAgent = useUserAgent();
@@ -158,7 +185,11 @@ export const ConnectionOptionInjected = ({
</span>
</ConnectionOptionButtonWithDescription>
) : (
<ConnectionOptionLinkWithDescription id={id} href={link}>
<ConnectionOptionLinkWithDescription
id={id}
href={link}
onClick={onInstall}
>
<span className="flex flex-col justify-start text-left">
<span className="capitalize leading-5">
{t('Get the Vega Wallet')}
@@ -183,7 +214,7 @@ export const ConnectionOptionInjected = ({
{name}
</ConnectionOptionButton>
) : (
<ConnectionOptionLink id={id} href={link}>
<ConnectionOptionLink id={id} href={link} onClick={onInstall}>
{t('Get the Vega Wallet')}
</ConnectionOptionLink>
)}
@@ -275,8 +306,9 @@ const ConnectionOptionLink = forwardRef<
children: ReactNode;
id: ConnectorType;
href: string;
onClick?: () => void;
}
>(({ children, id, href }, ref) => {
>(({ children, id, href, onClick }, ref) => {
return (
<a
href={href}
@@ -285,6 +317,7 @@ const ConnectionOptionLink = forwardRef<
className={CONNECTION_OPTION_CLASSES}
data-testid={`connector-${id}`}
ref={ref}
onClick={onClick}
>
<ConnectorIcon id={id} />
{children}
@@ -320,8 +353,10 @@ const ConnectionOptionLinkWithDescription = forwardRef<
children: ReactNode;
id: ConnectorType;
href: string;
onClick?: () => void;
}
>(({ children, id, href }, ref) => {
>(({ children, id, href, onClick }, ref) => {
return (
<a
ref={ref}
@@ -329,6 +364,7 @@ const ConnectionOptionLinkWithDescription = forwardRef<
href={href}
target="_blank"
rel="noreferrer"
onClick={onClick}
>
<span>
<ConnectorIcon id={id} />
@@ -1,17 +1,20 @@
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useWallet } from './use-wallet';
import { useConnect } from './use-connect';
export function useEagerConnect() {
const current = useWallet((store) => store.current);
const status = useWallet((store) => store.status);
const { connect } = useConnect();
const [connecting, setConnecting] = useState(true);
useEffect(() => {
const attemptConnect = async () => {
// No stored config, or config was malformed or no risk accepted
if (!current) {
setConnecting(false);
return;
}
if (status !== 'disconnected') {
return;
}
@@ -19,15 +22,13 @@ export function useEagerConnect() {
await connect(current);
} catch {
console.warn(`Failed to connect with connector: ${current}`);
} finally {
setConnecting(false);
}
};
if (typeof window !== 'undefined') {
attemptConnect();
}
}, [connect, current, connecting]);
}, [status, connect, current]);
return connecting;
return status;
}
@@ -33,6 +33,12 @@ export const useSimpleTransaction = (opts?: Options) => {
const [result, setResult] = useState<Result>();
const [error, setError] = useState<string>();
const reset = () => {
setStatus('idle');
setResult(undefined);
setError(undefined);
};
const send = async (tx: Transaction) => {
if (!pubKey) {
throw new Error('no pubKey');
@@ -59,12 +65,12 @@ export const useSimpleTransaction = (opts?: Options) => {
if (err.code === ConnectorErrors.userRejected.code) {
setStatus('idle');
} else {
setError(err.message);
setError(`${err.message}${err.data ? `: ${err.data}` : ''}`);
setStatus('idle');
opts?.onError?.(err.message);
}
} else {
const msg = t('Wallet rejected transaction');
const msg = t('Something went wrong');
setError(msg);
setStatus('idle');
opts?.onError?.(msg);
@@ -114,5 +120,6 @@ export const useSimpleTransaction = (opts?: Options) => {
error,
status,
send,
reset,
};
};
@@ -7,6 +7,7 @@ import {
listKeysError,
noWalletError,
sendTransactionError,
userRejectedError,
} from '../errors';
import {
type TransactionParams,
@@ -14,6 +15,19 @@ import {
type VegaWalletEvent,
} from '../types';
interface InjectedError {
message: string;
code: number;
data:
| {
message: string;
code: number;
}
| string;
}
const USER_REJECTED_CODE = -4;
export class InjectedConnector implements Connector {
readonly id = 'injected';
readonly name = 'Vega Wallet';
@@ -85,15 +99,55 @@ export class InjectedConnector implements Connector {
sentAt: res.sentAt,
};
} catch (err) {
if (this.isInjectedError(err)) {
if (err.code === USER_REJECTED_CODE) {
throw userRejectedError();
}
if (typeof err.data === 'string') {
throw sendTransactionError(err.data);
} else {
throw sendTransactionError(err.data.message);
}
}
throw sendTransactionError();
}
}
on(event: VegaWalletEvent, callback: () => void) {
window.vega.on(event, callback);
// Check for on/off in case user is on older versions which don't support it
// We can remove this check once FF is at the latest version
if (
typeof window.vega !== 'undefined' &&
typeof window.vega.on === 'function'
) {
window.vega.on(event, callback);
}
}
off(event: VegaWalletEvent, callback: () => void) {
window.vega.off(event, callback);
// Check for on/off in case user is on older versions which don't support it
// We can remove this check once FF is at the latest version
if (
typeof window.vega !== 'undefined' &&
typeof window.vega.off === 'function'
) {
window.vega.off(event, callback);
}
}
private isInjectedError(obj: unknown): obj is InjectedError {
if (
obj !== undefined &&
obj !== null &&
typeof obj === 'object' &&
'code' in obj &&
'message' in obj &&
'data' in obj
) {
return true;
}
return false;
}
}
@@ -17,6 +17,8 @@ import {
type JsonRpcConnectorConfig = { url: string; token?: string };
const USER_REJECTED_CODE = 3001;
export class JsonRpcConnector implements Connector {
readonly id = 'jsonRpc';
readonly name = 'Command Line Wallet';
@@ -27,7 +29,7 @@ export class JsonRpcConnector implements Connector {
requestId: number = 0;
store: StoreApi<Store> | undefined;
pollRef: NodeJS.Timer | undefined;
ee: EventEmitter;
ee: InstanceType<typeof EventEmitter>;
constructor(config: JsonRpcConnectorConfig) {
this.url = config.url;
@@ -63,7 +65,7 @@ export class JsonRpcConnector implements Connector {
const token = response.headers.get('Authorization');
if (!response.ok) {
if ('error' in data && data.error.code === 3001) {
if ('error' in data && data.error.code === USER_REJECTED_CODE) {
throw userRejectedError();
}
throw connectError('response not ok');
@@ -137,7 +139,7 @@ export class JsonRpcConnector implements Connector {
if (!response.ok) {
if ('error' in data) {
if (data.error.code === 3001) {
if (data.error.code === USER_REJECTED_CODE) {
throw userRejectedError();
}
+102 -97
View File
@@ -1,4 +1,3 @@
import EventEmitter from 'eventemitter3';
import {
ConnectorError,
chainIdError,
@@ -6,13 +5,13 @@ import {
listKeysError,
noWalletError,
sendTransactionError,
userRejectedError,
} from '../errors';
import { type Transaction } from '../transaction-types';
import {
JsonRpcMethod,
type Connector,
type TransactionParams,
type VegaWalletEvent,
} from '../types';
enum EthereumMethod {
@@ -43,7 +42,6 @@ declare global {
type WindowEthereumProvider = {
isMetaMask: boolean;
request<T = unknown>(args: RequestArguments): Promise<T>;
selectedAddress: string | null;
};
interface Window {
@@ -52,6 +50,16 @@ declare global {
}
}
interface SnapRPCError {
code: number;
message: string;
data?: {
originalError: { code: number };
};
}
const USER_REJECTED_CODE = -4;
export class SnapConnector implements Connector {
readonly id = 'snap';
readonly name = 'MetaMask Snap';
@@ -61,8 +69,6 @@ export class SnapConnector implements Connector {
node: string;
version: string;
snapId: string;
pollRef: NodeJS.Timer | undefined;
ee: EventEmitter;
// Note: apps may not know which node is selected on start up so its up
// to the app to make sure class intances are renewed if the node changes
@@ -70,14 +76,21 @@ export class SnapConnector implements Connector {
this.node = config.node;
this.version = config.version;
this.snapId = config.snapId;
this.ee = new EventEmitter();
}
bindStore() {}
async connectWallet(desiredChainId: string) {
try {
await this.requestSnap();
const res = await this.requestSnap();
if (res[this.snapId].blocked) {
throw connectError('snap is blocked');
}
if (!res[this.snapId].enabled) {
throw connectError('snap is not enabled');
}
const { chainId } = await this.getChainId();
@@ -87,7 +100,6 @@ export class SnapConnector implements Connector {
);
}
this.startPoll();
return { success: true };
} catch (err) {
if (err instanceof ConnectorError) {
@@ -98,57 +110,66 @@ export class SnapConnector implements Connector {
}
}
async disconnectWallet() {
this.stopPoll();
}
async disconnectWallet() {}
// deprecated, pass chain on connect
async getChainId() {
try {
const res = await this.invokeSnap<{ chainID: string }>(
const data = await this.invokeSnap<{ chainID: string }>(
JsonRpcMethod.GetChainId,
{
networkEndpoints: [this.node],
}
);
return { chainId: res.chainID };
if ('error' in data) {
throw chainIdError(data.error.message);
}
return { chainId: data.chainID };
} catch (err) {
this.stopPoll();
if (err instanceof ConnectorError) {
throw err;
}
throw chainIdError();
}
}
async listKeys() {
try {
const res = await this.invokeSnap<{
const data = await this.invokeSnap<{
keys: Array<{ publicKey: string; name: string }>;
}>(JsonRpcMethod.ListKeys);
return res.keys;
if ('error' in data) {
throw listKeysError(data.error.message);
}
return data.keys;
} catch (err) {
this.stopPoll();
if (err instanceof ConnectorError) {
throw err;
}
throw listKeysError();
}
}
async isConnected() {
try {
// Check if metamask is unlocked
if (!window.ethereum.selectedAddress) {
throw noWalletError();
}
// If this throws its likely the snap is disabled or has been uninstalled
await this.listKeys();
return { connected: true };
} catch (err) {
this.stopPoll();
return { connected: false };
}
}
async sendTransaction(params: TransactionParams) {
try {
const res = await this.invokeSnap<{
// If the transaction is invalid this will throw with SnapRPCError
// but if its rejected it will resolve with 'error' in data
const data = await this.invokeSnap<{
transactionHash: string;
transaction: { signature: { value: string } };
receivedAt: string;
@@ -160,115 +181,99 @@ export class SnapConnector implements Connector {
networkEndpoints: [this.node],
});
if ('error' in data) {
if (data.error.code === USER_REJECTED_CODE) {
throw userRejectedError();
}
throw sendTransactionError(`${data.error.message}: ${data.error.data}`);
}
return {
transactionHash: res.transactionHash,
signature: res.transaction.signature.value,
receivedAt: res.receivedAt,
sentAt: res.sentAt,
transactionHash: data.transactionHash,
signature: data.transaction.signature.value,
receivedAt: data.receivedAt,
sentAt: data.sentAt,
};
} catch (err) {
if (err instanceof ConnectorError) {
throw err;
}
if (this.isSnapRPCError(err)) {
throw sendTransactionError(err.message);
}
throw sendTransactionError();
}
}
on(event: VegaWalletEvent, callback: () => void) {
this.ee.on(event, callback);
}
on() {}
off() {}
off(event: VegaWalletEvent, callback?: () => void) {
this.ee.off(event, callback);
}
////////////////////////////////////
// Snap methods
////////////////////////////////////
private startPoll() {
// This only event we need to poll for right now is client.disconnect,
// if more events get added we will need more logic here
this.pollRef = setInterval(async () => {
const result = await this.isConnected();
if (result.connected) return;
this.ee.emit('client.disconnected');
}, 2000);
}
private stopPoll() {
if (this.pollRef) {
clearInterval(this.pollRef);
}
}
/**
* Requests permission for a website to communicate with the specified snaps
* and attempts to install them if they're not already installed.
* If the installation of any snap fails, returns the error that caused the failure.
* More informations here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_requestsnaps
*/
private async requestSnap() {
await this.request(EthereumMethod.RequestSnaps, {
[this.snapId]: {
version: this.version,
private async requestSnap(): Promise<{
[snapId: string]: {
blocked: boolean;
enabled: boolean;
id: string;
version: string;
};
}> {
return window.ethereum.request({
method: EthereumMethod.RequestSnaps,
params: {
[this.snapId]: {
version: this.version,
},
},
});
}
// TODO: check if this is needed, its used in use-snap-status
//
//
// /**
// * Gets the list of all installed snaps.
// * More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps
// */
// async getSnap() {
// const snaps = await this.request(EthereumMethod.GetSnaps);
// return Object.values(snaps).find(
// (s) => s.id === this.snapId && s.version === this.version
// );
// }
/**
* Calls a method on the specified snap, always vega in this case
* should always be npm:@vegaprotocol/snap
*/
private async invokeSnap<TResult>(
method: JsonRpcMethod,
params?: SnapInvocationParams
): Promise<TResult> {
return await this.request(EthereumMethod.InvokeSnap, {
snapId: this.snapId,
request: {
method,
params,
params: SnapInvocationParams = {}
): Promise<TResult | { error: SnapRPCError }> {
// MetaMask in Firefox doesn't like undefined properties or some properties
// on __proto__ so we need to strip them out with JSON.strinfify
params = JSON.parse(JSON.stringify(params));
return window.ethereum.request({
method: EthereumMethod.InvokeSnap,
params: {
snapId: this.snapId,
request: {
method,
params,
},
},
});
}
/**
* Calls window.ethereum.request with method and params
*/
private async request<TResult>(
method: EthereumMethod,
params?: object
): Promise<TResult> {
if (window.ethereum?.request && window.ethereum?.isMetaMask) {
// MetaMask in Firefox doesn't like undefined properties or some properties
// on __proto__ so we need to strip them out with JSON.strinfify
try {
params = JSON.parse(JSON.stringify(params));
} catch (err) {
throw sendTransactionError();
}
return window.ethereum.request({
method,
params,
});
private isSnapRPCError(obj: unknown): obj is SnapRPCError {
if (
obj !== undefined &&
obj !== null &&
typeof obj === 'object' &&
'code' in obj &&
'message' in obj
) {
return true;
}
throw noWalletError();
return false;
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ export class ConnectorError extends Error {
export const ConnectorErrors = {
userRejected: { message: 'user rejected', code: 0 },
noConnector: { message: 'no connector', code: 1 },
noConnector: { message: 'not connected', code: 1 },
connect: { message: 'failed to connect', code: 2 },
disconnect: { message: 'failed to disconnect', code: 3 },
chainId: { message: 'incorrect chain id', code: 4 },
+10 -1
View File
@@ -492,6 +492,14 @@ export interface UpdateMarginMode {
export interface UpdateMarginModeBody {
updateMarginMode: UpdateMarginMode;
}
export interface UpdatePartyProfile {
updatePartyProfile: {
alias: string;
metadata: Array<{ key: string; value: string }>;
};
}
export type Transaction =
| UpdateMarginModeBody
| StopOrdersSubmissionBody
@@ -510,7 +518,8 @@ export type Transaction =
| ApplyReferralCode
| JoinTeam
| CreateReferralSet
| UpdateReferralSet;
| UpdateReferralSet
| UpdatePartyProfile;
export interface TransactionResponse {
transactionHash: string;
+1 -2
View File
@@ -93,7 +93,6 @@ describe('disconnect', () => {
expect(result).toEqual({ status: 'disconnected' });
expect(config.store.getState()).toMatchObject({
status: 'disconnected',
error: noConnectorError(),
current: undefined,
keys: [],
pubKey: undefined,
@@ -130,7 +129,7 @@ describe('refresh keys', () => {
it('handles invalid connector', async () => {
await config.refreshKeys();
expect(config.store.getState()).toMatchObject({
error: noConnectorError(),
error: undefined,
});
});
+9 -7
View File
@@ -132,18 +132,20 @@ export function createConfig(cfg: Config): Wallet {
store.setState(getInitialState(), true);
return { status: 'disconnected' as const };
} catch (err) {
store.setState({
...getInitialState(),
error: err instanceof ConnectorError ? err : unknownError(),
});
store.setState(getInitialState(), true);
return { status: 'disconnected' as const };
}
}
async function refreshKeys() {
const connector = connectors
.getState()
.find((x) => x.id === store.getState().current);
const state = store.getState();
const connector = connectors.getState().find((x) => x.id === state.current);
// Only refresh keys if connnected. If you aren't connect when you connect
// you will get the latest keys
if (state.status !== 'connected') {
return;
}
try {
if (!connector) {
-1
View File
@@ -11,7 +11,6 @@
"build:all": "nx run-many --all --target=build",
"build-spec:all": "nx run-many --all --target=build-spec",
"lint:all": "nx run-many --all --target=lint",
"e2e:all": "nx run-many --all --target=e2e",
"vegacapsule": "vegacapsule network bootstrap --config-path=../frontend-monorepo/vegacapsule/config.hcl",
"release": "git checkout develop ; git pull ; node scripts/make-release.js",
"trading:test": "cd apps/trading/e2e && poetry run pytest -k",
+44 -35
View File
@@ -7,18 +7,18 @@ projects = []
projects_e2e = []
previews = {
'governance': 'not deployed',
'explorer': 'not deployed',
'trading': 'not deployed',
'tools': 'not deployed',
'governance': 'not deployed',
'explorer': 'not deployed',
'trading': 'not deployed',
'tools': 'not deployed',
}
main_apps = ['governance', 'explorer', 'trading']
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
preview_governance = "not deployed"
preview_trading = "not deployed"
preview_explorer = "not deployed"
preview_tools = "not deployed"
# take input from the pipeline
parser = ArgumentParser()
@@ -30,7 +30,8 @@ parser.add_argument('--event-name', help='name of event in CI')
args = parser.parse_args()
# run yarn affected command
affected=check_output(f'yarn nx print-affected --base={environ["NX_BASE"]} --head={environ["NX_HEAD"]} --select=projects'.split()).decode('utf-8')
affected = check_output(
f'yarn nx print-affected --base={environ["NX_BASE"]} --head={environ["NX_HEAD"]} --select=projects'.split()).decode('utf-8')
# print useful information
@@ -44,46 +45,54 @@ print(affected)
print(">>>> eof debug")
# define affection actions -> add to projects arrays and generate preview link
def affect_app(app, preview_name=None):
print(f"{app} is affected")
projects.append(app)
if not preview_name:
preview_name=app
previews[app] = f'https://{preview_name}.{args.branch_slug}.vega.rocks'
print(f"{app} is affected")
projects.append(app)
if not preview_name:
preview_name = app
previews[app] = f'https://{preview_name}.{args.branch_slug}.vega.rocks'
# check appearance in the affected string for main apps
for app in main_apps:
if app in affected:
affect_app(app)
if app in affected:
affect_app(app)
# if non of main apps is affected - test all of them
if not projects:
for app in main_apps:
affect_app(app)
for app in main_apps:
affect_app(app)
# generate e2e targets
projects_e2e = [f'{app}-e2e' for app in projects]
# remove trading-e2e because it doesn't exists any more (new target is: console-e2e)
if "trading-e2e" in projects_e2e:
projects_e2e.remove("trading-e2e")
# check affection for multisig-signer which is deployed only from develop and pull requests
if args.event_name == 'pull_request' or 'develop' in args.github_ref:
if 'multisig-signer' in affected:
affect_app('multisig-signer', 'tools')
if 'multisig-signer' in affected:
affect_app('multisig-signer', 'tools')
# now parse apps that are deployed from develop but don't have previews
if 'develop' in args.github_ref:
for app in ['static', 'ui-toolkit']:
if app in affected:
projects.append(app)
for app in ['static', 'ui-toolkit']:
if app in affected:
projects.append(app)
# if ref is in format release/{env}-{app} then only {app} is deployed
if 'release' in args.github_ref:
for app in main_apps:
if f'{args.github_ref}'.endswith(app):
projects = [app]
projects_e2e = [f'{app}-e2e']
for app in main_apps:
if f'{args.github_ref}'.endswith(app):
projects = [app]
projects_e2e = [f'{app}-e2e']
projects = json.dumps(projects)
projects_e2e = json.dumps(projects_e2e)
print(f'Projects: {projects}')
@@ -91,20 +100,20 @@ print(f'Projects E2E: {projects_e2e}')
print('>> Previews')
for preview, preview_value in previews.items():
print(f'{preview}: {preview_value}')
print(f'{preview}: {preview_value}')
print('>> EOF Previews')
lines_to_write = [
f'PREVIEW_GOVERNANCE={previews["governance"]}',
f'PREVIEW_EXPLORER={previews["explorer"]}',
f'PREVIEW_TRADING={previews["trading"]}',
f'PREVIEW_TOOLS={previews["tools"]}',
f'PROJECTS={projects}',
f'PROJECTS_E2E={projects_e2e}',
f'PREVIEW_GOVERNANCE={previews["governance"]}',
f'PREVIEW_EXPLORER={previews["explorer"]}',
f'PREVIEW_TRADING={previews["trading"]}',
f'PREVIEW_TOOLS={previews["tools"]}',
f'PROJECTS={projects}',
f'PROJECTS_E2E={projects_e2e}',
]
env_file = environ['GITHUB_ENV']
print(f'Line to add to GITHUB_ENV file: {env_file}')
print(lines_to_write)
with open(env_file, 'a') as _f:
_f.write('\n'.join(lines_to_write))
_f.write('\n'.join(lines_to_write))