From ca64516a5223c3d1f0d4ababc98600ff14f185a5 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:08:22 +0200 Subject: [PATCH 01/21] fix(trading): lp data provider reload - updating book (#5846) --- .../components/liquidity-container/liquidity-container.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/trading/components/liquidity-container/liquidity-container.tsx b/apps/trading/components/liquidity-container/liquidity-container.tsx index 7ddea435b..2ac8e95d1 100644 --- a/apps/trading/components/liquidity-container/liquidity-container.tsx +++ b/apps/trading/components/liquidity-container/liquidity-container.tsx @@ -4,7 +4,6 @@ import { lpAggregatedDataProvider, type Filter, LiquidityTable, - liquidityProvisionsDataProvider, } from '@vegaprotocol/liquidity'; import { getAsset, useMarket } from '@vegaprotocol/markets'; import { @@ -71,7 +70,7 @@ export const LiquidityContainer = ({ const useReloadLiquidityData = (marketId: string | undefined) => { const { reload } = useDataProvider({ - dataProvider: liquidityProvisionsDataProvider, + dataProvider: lpAggregatedDataProvider, variables: { marketId: marketId || '' }, update: () => true, skip: !marketId, From 72e0cb76aa2cdf41c400a0b0bd5bd163945df714 Mon Sep 17 00:00:00 2001 From: Art Date: Mon, 26 Feb 2024 10:48:29 +0100 Subject: [PATCH 02/21] feat(governance): batch proposal indicators, vote breakdown ui changes (#5857) --- .../src/components/heading/heading.tsx | 3 +- .../proposal-header.tsx | 9 +- .../proposal-market-changes.spec.tsx | 5 + .../proposal-market-changes.tsx | 9 +- .../proposal-update-market-state.spec.tsx | 5 + .../proposal-update-market-state.tsx | 23 +- .../proposals/components/proposal/colours.ts | 33 ++- .../vote-breakdown/vote-breakdown.tsx | 274 ++++++++++-------- 8 files changed, 216 insertions(+), 145 deletions(-) diff --git a/apps/governance/src/components/heading/heading.tsx b/apps/governance/src/components/heading/heading.tsx index fec75b507..132d2c2a9 100644 --- a/apps/governance/src/components/heading/heading.tsx +++ b/apps/governance/src/components/heading/heading.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; +import { type ReactNode } from 'react'; interface HeadingProps { - title?: string; + title?: ReactNode; centerContent?: boolean; marginTop?: boolean; marginBottom?: boolean; diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx index dc0f676e4..c9eb079e5 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx @@ -54,11 +54,8 @@ const ProposalTypeTags = ({ if (proposal.__typename === 'BatchProposal') { return ( -
- {proposal.subProposals?.map((subProposal, i) => { - if (!subProposal?.terms) return null; - return ; - })} +
+ BatchProposal
); } @@ -315,7 +312,7 @@ const ProposalDetails = ({ {proposal.subProposals.map((p, i) => { if (!p?.terms) return null; return ( -
  • +
  • {i + 1}
    {renderDetails(p.terms)}
    diff --git a/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.spec.tsx index 10ac38937..4dddbed33 100644 --- a/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.spec.tsx @@ -5,6 +5,11 @@ import { } from './proposal-market-changes'; import type { JsonValue } from '../../../../components/json-diff'; +jest.mock('../proposal/market-name.tsx', () => ({ + ...jest.requireActual('../proposal/market-name.tsx'), + MarketName: jest.fn(), +})); + describe('applyImmutableKeysFromEarlierVersion', () => { it('returns an empty object if any argument is not an object or null', () => { const earlierVersion: JsonValue = null; diff --git a/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.tsx b/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.tsx index 1b0df685d..f72ac5f1a 100644 --- a/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-market-changes/proposal-market-changes.tsx @@ -18,6 +18,7 @@ import { type SingleProposalData, type SubProposalData, } from '../proposal/proposal-utils'; +import { MarketName } from '../proposal/market-name'; const immutableKeys = [ 'decimalPlaces', @@ -139,7 +140,13 @@ export const ProposalMarketChanges = ({ setToggleState={setShowChanges} dataTestId={'proposal-market-changes-toggle'} > - + + {t('UpdateToMarket')}: + + } + /> {showChanges && ( diff --git a/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.spec.tsx index 0e63817a7..263d89303 100644 --- a/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.spec.tsx @@ -2,6 +2,11 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { ProposalUpdateMarketState } from './proposal-update-market-state'; import { MarketUpdateType } from '@vegaprotocol/types'; +jest.mock('../proposal/market-name.tsx', () => ({ + ...jest.requireActual('../proposal/market-name.tsx'), + MarketName: jest.fn(), +})); + describe('', () => { const suspendProposal = { __typename: 'UpdateMarketState' as const, diff --git a/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.tsx b/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.tsx index 3351e5bbc..a666be169 100644 --- a/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-update-market-state/proposal-update-market-state.tsx @@ -10,6 +10,7 @@ import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { SubHeading } from '../../../../components/heading'; import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals'; import { MarketUpdateTypeMapping } from '@vegaprotocol/types'; +import { MarketName } from '../proposal/market-name'; interface ProposalUpdateMarketStateProps { change: UpdateMarketStatesFragment | null; @@ -20,16 +21,18 @@ export const ProposalUpdateMarketState = ({ }: ProposalUpdateMarketStateProps) => { const { t } = useTranslation(); const [showDetails, setShowDetails] = useState(false); - let market; - let isTerminate = false; - if (!change) { + if (!change || change.__typename !== 'UpdateMarketState') { return null; } - if (change.__typename === 'UpdateMarketState') { - market = change?.market; - isTerminate = change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE'; + const market = change?.market; + const isTerminate = + change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE'; + + let toggleTitle = t(change.updateType); + if (toggleTitle.length === 0) { + toggleTitle = t('MarketDetails'); } return ( @@ -39,7 +42,13 @@ export const ProposalUpdateMarketState = ({ setToggleState={setShowDetails} dataTestId="proposal-market-data-toggle" > - + + {toggleTitle}: + + } + /> {showDetails && ( diff --git a/apps/governance/src/routes/proposals/components/proposal/colours.ts b/apps/governance/src/routes/proposals/components/proposal/colours.ts index 4a42d0008..f88ad5713 100644 --- a/apps/governance/src/routes/proposals/components/proposal/colours.ts +++ b/apps/governance/src/routes/proposals/components/proposal/colours.ts @@ -16,18 +16,31 @@ const getColour = (indicator: number, max = COLOURS.length) => { export const getStyle = (indicator: number, max = COLOURS.length) => classNames({ - 'bg-vega-yellow-400': 'yellow' === getColour(indicator, max), - 'bg-vega-green-400': 'green' === getColour(indicator, max), - 'bg-vega-blue-400': 'blue' === getColour(indicator, max), - 'bg-vega-purple-400': 'purple' === getColour(indicator, max), - 'bg-vega-pink-400': 'pink' === getColour(indicator, max), - 'bg-vega-orange-400': 'orange' === getColour(indicator, max), - 'bg-vega-red-400': 'red' === getColour(indicator, max), - 'bg-vega-clight-600': 'none' === getColour(indicator, max), + 'bg-vega-yellow-400 after:bg-vega-yellow-400': + 'yellow' === getColour(indicator, max), + 'bg-vega-green-400 after:bg-vega-green-400': + 'green' === getColour(indicator, max), + 'bg-vega-blue-400 after:bg-vega-blue-400': + 'blue' === getColour(indicator, max), + 'bg-vega-purple-400 after:bg-vega-purple-400': + 'purple' === getColour(indicator, max), + 'bg-vega-pink-400 after:bg-vega-pink-400': + 'pink' === getColour(indicator, max), + 'bg-vega-orange-400 after:bg-vega-orange-400': + 'orange' === getColour(indicator, max), + 'bg-vega-red-400 after:bg-vega-red-400': + 'red' === getColour(indicator, max), + 'bg-vega-clight-600 after:bg-vega-clight-600': + 'none' === getColour(indicator, max), }); export const getIndicatorStyle = (indicator: number) => classNames( - 'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8', - getStyle(indicator) + 'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8 w-7 text-center', + 'text-border-1', + getStyle(indicator), + // Comment below if you want to remove the "chevron" + 'relative mr-[11px]', + 'after:absolute after:z-[-1] after:top-1 after:right-[-11px] after:rounded-sm', + "after:w-[22.62px] after:h-[22.62px] after:rotate-45 after:content-['']" ); diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx index 30a965bf1..2564180b9 100644 --- a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx @@ -16,6 +16,7 @@ import { } from '../../__generated__/Proposals'; import { useBatchVoteInformation } from '../../hooks/use-vote-information'; import { getIndicatorStyle } from '../proposal/colours'; +import { MarketName } from '../proposal/market-name'; export const CompactVotes = ({ number }: { number: BigNumber }) => ( { const containerClasses = classNames( - 'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden', - colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400' + 'relative h-2 rounded-md overflow-hidden', + // 'border border-vega-dark-300', + colourfulBg ? 'bg-vega-red' : 'bg-vega-dark-200' ); const progressClasses = classNames( @@ -50,17 +52,19 @@ const VoteProgress = ({ ); const textClasses = classNames( - 'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black' + 'w-full flex items-center justify-start text-white text-sm pb-1' ); return ( -
    -
    +
    {children}
    +
    +
    +
    ); }; @@ -79,14 +83,22 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
    {reached ? (
    - + {threshold.toString()}% {text} {t('met')}
    ) : (
    - + {threshold.toString()}% {text} {t('not met')} @@ -152,7 +164,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {

    {t( @@ -215,7 +227,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {

    {t('Proposal failed: {{count}} of {{total}} proposals passed', { @@ -237,6 +249,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => { if (!p?.terms) return null; return ( + : + + ) : null; + const indicatorElement = indicator && ( {indicator} ); return ( -

    -
    +
    +
    {indicatorElement} -

    {t(terms.change.__typename)}

    +

    + {t(terms.change.__typename)} {marketName} +

    +
    +
    +
    -
    ); }; @@ -302,11 +333,13 @@ const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => { const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket'; return ( - +
    + +
    ); }; @@ -370,13 +403,13 @@ const VoteBreakDownUI = ({ 'flex justify-between flex-wrap gap-6' ); const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow'); - const headingClasses = classNames('mb-2 text-vega-dark-400'); + const headingClasses = classNames('mb-2 text-sm text-white font-bold'); const progressDetailsClasses = classNames( 'flex justify-between flex-wrap mt-2 text-sm' ); return ( -
    +
    {isProposalOpen && (
    )} @@ -409,103 +442,13 @@ const VoteBreakDownUI = ({

    fail'} - components={[]} + components={[]} />

    )}
    )} - {isUpdateMarket && ( -
    -

    {t('liquidityProviderVote')}

    -
    -
    - - - - -
    -
    - {t('liquidityProviderVotesFor')}: - {lpVoteWeight.toFixed(defaultDP)}% - } - > - - -
    - -
    - {t('liquidityProviderVotesAgainst')}: - - {noLPPercentage.toFixed(defaultDP)}% - } - > - - - -
    -
    -
    - -
    - - - - -
    -
    - {t('totalLiquidityProviderTokensVoted')}: - - {totalEquityLikeShareWeight.toFixed(1)}% - -
    -
    -
    -
    -
    - )} - {isUpdateMarket &&

    {t('tokenVote')}

    }
    + + {/** Liquidity provider vote */} + {isUpdateMarket && ( +
    +

    {t('liquidityProviderVote')}

    +
    +
    + + + + +
    +
    + {t('liquidityProviderVotesFor')}: + {lpVoteWeight.toFixed(defaultDP)}% + } + > + + +
    + +
    + {t('liquidityProviderVotesAgainst')}: + + {noLPPercentage.toFixed(defaultDP)}% + } + > + + + +
    +
    +
    + +
    + + + + +
    +
    + {t('totalLiquidityProviderTokensVoted')}: + + {totalEquityLikeShareWeight.toFixed(1)}% + +
    +
    +
    +
    +
    + )}
    ); }; From 7101d49d1db6210e44b96efc333f3ce9b76cdee9 Mon Sep 17 00:00:00 2001 From: Dexter Edwards Date: Mon, 26 Feb 2024 15:57:09 +0300 Subject: [PATCH 03/21] chore(governance, trading): update links for new beta extension (#5858) --- apps/governance/.env | 2 +- apps/governance/.env.capsule | 2 +- apps/governance/.env.devnet | 2 +- apps/governance/.env.stagnet1 | 2 +- apps/governance/.env.testnet | 2 +- apps/governance/.env.validators-testnet | 2 +- apps/trading/.env | 2 +- apps/trading/.env.capsule | 2 +- apps/trading/.env.devnet | 2 +- apps/trading/.env.stagnet1 | 2 +- apps/trading/.env.testnet | 2 +- apps/trading/.env.validators-testnet | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/governance/.env b/apps/governance/.env index 09bfaeca4..d163f3e83 100644 --- a/apps/governance/.env +++ b/apps/governance/.env @@ -24,7 +24,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ #Test configuration variables CYPRESS_FAIRGROUND=false diff --git a/apps/governance/.env.capsule b/apps/governance/.env.capsule index 6c710d788..f60e8922b 100644 --- a/apps/governance/.env.capsule +++ b/apps/governance/.env.capsule @@ -20,7 +20,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_VEGA_REST_URL=http://localhost:3008/api/v2/ NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json diff --git a/apps/governance/.env.devnet b/apps/governance/.env.devnet index 5616a4821..b448ebf98 100644 --- a/apps/governance/.env.devnet +++ b/apps/governance/.env.devnet @@ -15,7 +15,7 @@ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket diff --git a/apps/governance/.env.stagnet1 b/apps/governance/.env.stagnet1 index b91995447..e7106b4df 100644 --- a/apps/governance/.env.stagnet1 +++ b/apps/governance/.env.stagnet1 @@ -13,7 +13,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket diff --git a/apps/governance/.env.testnet b/apps/governance/.env.testnet index 305473b45..21e74e358 100644 --- a/apps/governance/.env.testnet +++ b/apps/governance/.env.testnet @@ -18,7 +18,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket diff --git a/apps/governance/.env.validators-testnet b/apps/governance/.env.validators-testnet index a8f52c608..4cabe332e 100644 --- a/apps/governance/.env.validators-testnet +++ b/apps/governance/.env.validators-testnet @@ -14,7 +14,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega. diff --git a/apps/trading/.env b/apps/trading/.env index d1c361360..2c6c06afa 100644 --- a/apps/trading/.env +++ b/apps/trading/.env @@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 diff --git a/apps/trading/.env.capsule b/apps/trading/.env.capsule index 1dae7042b..f49446f32 100644 --- a/apps/trading/.env.capsule +++ b/apps/trading/.env.capsule @@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session" diff --git a/apps/trading/.env.devnet b/apps/trading/.env.devnet index 12da4f51d..48c7ab081 100644 --- a/apps/trading/.env.devnet +++ b/apps/trading/.env.devnet @@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json # Cosmic elevator flags diff --git a/apps/trading/.env.stagnet1 b/apps/trading/.env.stagnet1 index 8634b87af..6cb9bf821 100644 --- a/apps/trading/.env.stagnet1 +++ b/apps/trading/.env.stagnet1 @@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 diff --git a/apps/trading/.env.testnet b/apps/trading/.env.testnet index 491d12bb1..b8c6d8c57 100644 --- a/apps/trading/.env.testnet +++ b/apps/trading/.env.testnet @@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.fairground.wtf NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 diff --git a/apps/trading/.env.validators-testnet b/apps/trading/.env.validators-testnet index a24ba7326..b5ef5a6e7 100644 --- a/apps/trading/.env.validators-testnet +++ b/apps/trading/.env.validators-testnet @@ -16,7 +16,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json # Cosmic elevator flags From 3c6a806ad3c3eaacab1840926cba3fd4fa0134b1 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 Feb 2024 12:57:16 +0000 Subject: [PATCH 04/21] chore(trading): reuse vega (#5850) --- apps/trading/e2e/conftest.py | 24 ++++ .../e2e/tests/deal_ticket/test_basic.py | 110 +++++++++++------- .../test_fees_margin_estimations.py | 3 +- .../e2e/tests/deal_ticket/test_stop_order.py | 29 ++--- .../e2e/tests/navigation/test_navigation.py | 27 ++--- .../e2e/tests/order/test_order_status.py | 26 ++--- .../e2e/tests/settings/test_settings.py | 16 ++- 7 files changed, 139 insertions(+), 96 deletions(-) diff --git a/apps/trading/e2e/conftest.py b/apps/trading/e2e/conftest.py index 1574db6e9..c0b13df17 100644 --- a/apps/trading/e2e/conftest.py +++ b/apps/trading/e2e/conftest.py @@ -182,7 +182,27 @@ def vega(request): request.addfinalizer(lambda: cleanup_container(vega_instance)) yield vega_instance +@pytest.fixture(scope="session", autouse=True) +def shared_vega(request): + with init_vega(request) as vega_instance: + try: + request.addfinalizer(lambda: cleanup_container(vega_instance)) + yield vega_instance + finally: + cleanup_container(vega_instance) +@pytest.fixture +def page_shared_vega(shared_vega, browser, request): + with init_page(shared_vega, browser, request) as page_instance: + yield page_instance + +@pytest.fixture +def auth_shared_vega(shared_vega: VegaServiceNull, page_shared_vega: Page): + return auth_setup(shared_vega, page_shared_vega) + +@pytest.fixture +def risk_accepted_shared_vega(page_shared_vega: Page): + risk_accepted_setup(page_shared_vega) def cleanup_container(vega_instance): try: @@ -283,6 +303,10 @@ def opening_auction_market(vega): return setup_opening_auction_market(vega) +@pytest.fixture(scope="function") +def shared_continuous_market(shared_vega:VegaServiceNull): + return setup_continuous_market(shared_vega) + @pytest.fixture(scope="function") def continuous_market(vega): return setup_continuous_market(vega) diff --git a/apps/trading/e2e/tests/deal_ticket/test_basic.py b/apps/trading/e2e/tests/deal_ticket/test_basic.py index 0a97dff71..298db628c 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_basic.py +++ b/apps/trading/e2e/tests/deal_ticket/test_basic.py @@ -2,35 +2,62 @@ import pytest from playwright.sync_api import Page, expect from vega_sim.null_service import VegaServiceNull from datetime import datetime, timedelta -from conftest import init_vega, cleanup_container +from conftest import init_page, risk_accepted_setup, auth_setup from fixtures.market import setup_continuous_market from actions.utils import wait_for_toast_confirmation +from wallet_config import WalletConfig, MM_WALLET2 + +from actions.utils import ( + change_keys, + create_and_faucet_wallet, +) order_size = "order-size" order_price = "order-price" place_order = "place-order" order_side_sell = "order-side-SIDE_SELL" +order_side_buy = "order-side-SIDE_BUY" market_order = "order-type-Market" +limit_order = "order-type-Limit" tif = "order-tif" expire = "expire" -@pytest.fixture(scope="module") -def vega(request): - with init_vega(request) as vega_instance: - request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function - yield vega_instance +@pytest.fixture(scope="module") +def page(shared_vega, browser, request, continuous_market): + with init_page(shared_vega, browser, request) as page: + risk_accepted_setup(page) + auth_setup(shared_vega, page) + basic_key = WalletConfig("basic_key", "basic_key") + create_and_faucet_wallet(vega=shared_vega, wallet=basic_key) + page.goto(f"/#/markets/{continuous_market}") + change_keys(page, shared_vega, "basic_key") + yield page @pytest.fixture(scope="module") -def continuous_market(vega): - return setup_continuous_market(vega) +def continuous_market(shared_vega:VegaServiceNull): + keypairs = shared_vega.wallet.get_keypairs("MarketSim") + proposal_key = keypairs.get('market_maker') + termination_key=keypairs.get('FJMKnwfZdd48C8NqvYrG') + mm_2_key=keypairs.get('market_maker_2') + + kwargs = {} + if proposal_key is not None: + kwargs['proposal_key'] = proposal_key + if termination_key is not None: + kwargs['termination_key'] = termination_key + if mm_2_key is not None: + kwargs['mm_2_key'] = mm_2_key + + return setup_continuous_market(shared_vega, **kwargs) -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_limit_buy_order_GTT(continuous_market, vega: VegaServiceNull, page: Page): - page.goto(f"/#/markets/{continuous_market}") + +def test_limit_buy_order_GTT( shared_vega: VegaServiceNull, page: Page): + page.get_by_test_id(limit_order).click() + page.get_by_test_id(order_side_buy).click() page.get_by_test_id(tif).select_option("Good 'til Time (GTT)") page.get_by_test_id(order_size).fill("10") page.get_by_test_id(order_price).fill("120") @@ -47,30 +74,31 @@ def test_limit_buy_order_GTT(continuous_market, vega: VegaServiceNull, page: Pag ) page.get_by_test_id(place_order).click() wait_for_toast_confirmation(page) - vega.wait_fn(1) - vega.wait_for_total_catchup() + shared_vega.wait_fn(1) + shared_vega.wait_for_total_catchup() page.get_by_test_id("All").click() + page.reload() # 7002-SORD-017 - expect(page.get_by_role("row").nth(5)).to_contain_text("10+10LimitFilled120.00GTT:") + expect(page.get_by_role("row").nth(4)).to_contain_text("10+10LimitFilled120.00GTT:") -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_limit_buy_order(continuous_market, vega: VegaServiceNull, page: Page): - page.goto(f"/#/markets/{continuous_market}") +def test_limit_buy_order(shared_vega: VegaServiceNull, page: Page): + page.get_by_test_id(limit_order).click() + page.get_by_test_id(order_side_buy).click() page.get_by_test_id(order_size).fill("10") page.get_by_test_id(order_price).fill("120") page.get_by_test_id(place_order).click() wait_for_toast_confirmation(page) - vega.wait_fn(2) - vega.wait_for_total_catchup() + shared_vega.wait_fn(2) + shared_vega.wait_for_total_catchup() page.get_by_test_id("All").click() + page.reload() # 7002-SORD-017 - expect(page.get_by_role("row").nth(6)).to_contain_text("10+10LimitFilled120.00GTC") + expect(page.get_by_role("row").nth(5)).to_contain_text("10+10LimitFilled120.00GTT") -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_limit_sell_order(continuous_market, vega: VegaServiceNull, page: Page): - page.goto(f"/#/markets/{continuous_market}") +def test_limit_sell_order(shared_vega: VegaServiceNull, page: Page): + page.get_by_test_id(limit_order).click() page.get_by_test_id(order_size).fill("10") page.get_by_test_id(order_price).fill("100") page.get_by_test_id(order_side_sell).click() @@ -84,15 +112,15 @@ def test_limit_sell_order(continuous_market, vega: VegaServiceNull, page: Page): ) page.get_by_test_id(place_order).click() wait_for_toast_confirmation(page) - vega.wait_fn(1) - vega.wait_for_total_catchup() + shared_vega.wait_fn(1) + shared_vega.wait_for_total_catchup() page.get_by_test_id("All").click() - expect(page.get_by_role("row").nth(7)).to_contain_text("10-10LimitFilled100.00GFN") + page.reload() + expect(page.get_by_role("row").nth(6)).to_contain_text("10-10LimitFilled100.00GFN") -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_market_sell_order(continuous_market, vega: VegaServiceNull, page: Page): - page.goto(f"/#/markets/{continuous_market}") + +def test_market_sell_order(shared_vega: VegaServiceNull, page: Page): page.get_by_test_id(market_order).click() page.get_by_test_id(order_size).fill("10") page.get_by_test_id(order_side_sell).click() @@ -105,32 +133,32 @@ def test_market_sell_order(continuous_market, vega: VegaServiceNull, page: Page) ) page.get_by_test_id(place_order).click() wait_for_toast_confirmation(page) - vega.wait_fn(1) - vega.wait_for_total_catchup() + shared_vega.wait_fn(1) + shared_vega.wait_for_total_catchup() page.get_by_test_id("All").click() - expect(page.get_by_role("row").nth(8)).to_contain_text("10-10MarketFilled-IOC") + page.reload() + expect(page.get_by_role("row").nth(7)).to_contain_text("10-10MarketFilled-IOC") -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_market_buy_order(continuous_market, vega: VegaServiceNull, page: Page): - page.goto(f"/#/markets/{continuous_market}") +def test_market_buy_order(shared_vega: VegaServiceNull, page: Page): page.get_by_test_id(market_order).click() + page.get_by_test_id(order_side_buy).click() page.get_by_test_id(order_size).fill("10") page.get_by_test_id(tif).select_option("Fill or Kill (FOK)") page.get_by_test_id(place_order).click() wait_for_toast_confirmation(page) - vega.wait_fn(1) - vega.wait_for_total_catchup() + shared_vega.wait_fn(1) + shared_vega.wait_for_total_catchup() page.get_by_test_id("All").click() + page.reload() # 7002-SORD-010 # 0003-WTXN-012 # 0003-WTXN-003 - expect(page.get_by_role("row").nth(9)).to_contain_text("10+10MarketFilled-FOK") + expect(page.get_by_role("row").nth(8)).to_contain_text("10+10MarketFilled-FOK") -@pytest.mark.usefixtures("risk_accepted") -def test_sidebar_should_be_open_after_reload(continuous_market, page: Page): - page.goto(f"/#/markets/{continuous_market}") + +def test_sidebar_should_be_open_after_reload(page: Page): expect(page.get_by_test_id("deal-ticket-form")).to_be_visible() page.get_by_test_id("Order").click() expect(page.get_by_test_id("deal-ticket-form")).not_to_be_visible() diff --git a/apps/trading/e2e/tests/deal_ticket/test_fees_margin_estimations.py b/apps/trading/e2e/tests/deal_ticket/test_fees_margin_estimations.py index cd366048d..eea9691f9 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_fees_margin_estimations.py +++ b/apps/trading/e2e/tests/deal_ticket/test_fees_margin_estimations.py @@ -1,4 +1,4 @@ -import pytest +""" import pytest from playwright.sync_api import Page, expect from vega_sim.null_service import VegaServiceNull from actions.vega import submit_order @@ -87,3 +87,4 @@ def test_margin_and_fees_estimations(continuous_market, vega: VegaServiceNull, p # expect(page.get_by_test_id("toast-content")).to_contain_text( # "Your transaction has been confirmed" # ) + """ \ No newline at end of file diff --git a/apps/trading/e2e/tests/deal_ticket/test_stop_order.py b/apps/trading/e2e/tests/deal_ticket/test_stop_order.py index 6ae055806..7a6da5834 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_stop_order.py +++ b/apps/trading/e2e/tests/deal_ticket/test_stop_order.py @@ -44,25 +44,25 @@ def create_position(vega: VegaServiceNull, market_id): vega.wait_fn(1) vega.wait_for_total_catchup -@pytest.mark.usefixtures("auth", "risk_accepted") -def test_stop_order_form_error_validation(continuous_market, page: Page): +@pytest.mark.usefixtures("auth_shared_vega", "risk_accepted_shared_vega") +def test_stop_order_form_error_validation(shared_continuous_market, page_shared_vega: Page): # 7002-SORD-032 - page.goto(f"/#/markets/{continuous_market}") - page.get_by_test_id(stop_order_btn).click() - page.get_by_test_id(stop_limit_order_btn).is_visible() - page.get_by_test_id(stop_limit_order_btn).click() - page.get_by_test_id(order_side_sell).click() - page.get_by_test_id(submit_stop_order).click() - expect(page.get_by_test_id("stop-order-error-message-trigger-price")).to_have_text( + page_shared_vega.goto(f"/#/markets/{shared_continuous_market}") + page_shared_vega.get_by_test_id(stop_order_btn).click() + page_shared_vega.get_by_test_id(stop_limit_order_btn).is_visible() + page_shared_vega.get_by_test_id(stop_limit_order_btn).click() + page_shared_vega.get_by_test_id(order_side_sell).click() + page_shared_vega.get_by_test_id(submit_stop_order).click() + expect(page_shared_vega.get_by_test_id("stop-order-error-message-trigger-price")).to_have_text( "You need provide a price" ) - expect(page.get_by_test_id("stop-order-error-message-size")).to_have_text( + expect(page_shared_vega.get_by_test_id("stop-order-error-message-size")).to_have_text( "Size cannot be lower than 1" ) - page.get_by_test_id(order_size).fill("1") - page.get_by_test_id(order_price).fill("0.0000001") - expect(page.get_by_test_id("stop-order-error-message-price")).to_have_text( + page_shared_vega.get_by_test_id(order_size).fill("1") + page_shared_vega.get_by_test_id(order_price).fill("0.0000001") + expect(page_shared_vega.get_by_test_id("stop-order-error-message-price")).to_have_text( "Price cannot be lower than 0.00001" ) @@ -257,7 +257,7 @@ def test_submit_stop_limit_order_cancel( ).to_have_text("Cancelled") -class TestStopOcoValidation: +""" class TestStopOcoValidation: @pytest.fixture(scope="class") def vega(request): with init_vega(request) as vega_instance: @@ -296,3 +296,4 @@ class TestStopOcoValidation: expect(page.get_by_test_id("stop-order-warning-limit")).to_have_text( "There is a limit of 4 active stop orders per market. Orders submitted above the limit will be immediately rejected." ) + """ \ No newline at end of file diff --git a/apps/trading/e2e/tests/navigation/test_navigation.py b/apps/trading/e2e/tests/navigation/test_navigation.py index 461d74a3e..32369ff96 100644 --- a/apps/trading/e2e/tests/navigation/test_navigation.py +++ b/apps/trading/e2e/tests/navigation/test_navigation.py @@ -1,39 +1,30 @@ import pytest from playwright.sync_api import Page, expect, Locator - -from conftest import init_page, init_vega, cleanup_container +from conftest import init_page, risk_accepted_setup @pytest.fixture(scope="module") -def vega(request): - with init_vega(request) as vega_instance: - request.addfinalizer(lambda: cleanup_container(vega_instance)) - yield vega_instance - - -@pytest.fixture(scope="module") -def page(vega, browser, request): - with init_page(vega, browser, request) as page: +def page(shared_vega, browser, request): + with init_page(shared_vega, browser, request) as page: + risk_accepted_setup(page) + page.goto("/#/disclaimer") yield page -@pytest.mark.usefixtures("risk_accepted") + def test_network_switcher(page: Page): - page.goto("/#/disclaimer") navbar = page.locator('nav[aria-label="Main"]') assert_network_switcher(navbar) -@pytest.mark.usefixtures("risk_accepted") + def test_navbar_pages(page: Page): - page.goto("/#/disclaimer") navbar = page.locator('nav[aria-label="Main"]') assert_links(navbar) -@pytest.mark.usefixtures("risk_accepted") + def test_navigation_mobile(page: Page): - page.goto("/#/disclaimer") page.set_viewport_size({"width": 800, "height": 1040}) navbar = page.locator('nav[aria-label="Main"]') @@ -108,4 +99,4 @@ def assert_network_switcher(container: Locator): expect(mainnet_link).to_be_visible() # 0006-NETW-003 expect(mainnet_link).to_have_attribute("href", "https://console.vega.xyz") - expect(container.get_by_role("link", name="Fairground testnet")).to_be_visible() + expect(container.get_by_role("link", name="Fairground testnet")).to_be_visible() \ No newline at end of file diff --git a/apps/trading/e2e/tests/order/test_order_status.py b/apps/trading/e2e/tests/order/test_order_status.py index 69be71638..5f685d033 100644 --- a/apps/trading/e2e/tests/order/test_order_status.py +++ b/apps/trading/e2e/tests/order/test_order_status.py @@ -49,7 +49,7 @@ def markets(vega: VegaServiceNull): price=130, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -63,7 +63,7 @@ def markets(vega: VegaServiceNull): price=88, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -77,7 +77,7 @@ def markets(vega: VegaServiceNull): price=88, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -92,7 +92,7 @@ def markets(vega: VegaServiceNull): wait=False, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -105,7 +105,7 @@ def markets(vega: VegaServiceNull): volume=100, price=104, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -120,7 +120,7 @@ def markets(vega: VegaServiceNull): expires_at=vega.get_blockchain_time() + 5 * 1e9, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -134,7 +134,7 @@ def markets(vega: VegaServiceNull): volume=20, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -148,7 +148,7 @@ def markets(vega: VegaServiceNull): volume=40, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -162,7 +162,7 @@ def markets(vega: VegaServiceNull): volume=60, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -177,7 +177,7 @@ def markets(vega: VegaServiceNull): volume=60, ) - vega.forward("2s") + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -190,7 +190,7 @@ def markets(vega: VegaServiceNull): volume=10, price=150, ) - + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -203,7 +203,7 @@ def markets(vega: VegaServiceNull): volume=10, price=160, ) - + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() @@ -216,7 +216,7 @@ def markets(vega: VegaServiceNull): volume=10, price=60, ) - + vega.forward("5s") vega.wait_fn(1) vega.wait_for_total_catchup() diff --git a/apps/trading/e2e/tests/settings/test_settings.py b/apps/trading/e2e/tests/settings/test_settings.py index aa0d3f609..819e3ea4f 100644 --- a/apps/trading/e2e/tests/settings/test_settings.py +++ b/apps/trading/e2e/tests/settings/test_settings.py @@ -1,16 +1,15 @@ import pytest from playwright.sync_api import expect, Page -from conftest import init_vega, cleanup_container +from conftest import init_page, risk_accepted_setup @pytest.fixture(scope="module") -def vega(request): - with init_vega(request) as vega_instance: - request.addfinalizer(lambda: cleanup_container(vega_instance)) - yield vega_instance +def page(shared_vega, browser, request): + with init_page(shared_vega, browser, request) as page: + risk_accepted_setup(page) + yield page -@pytest.mark.usefixtures("risk_accepted") def test_share_usage_data(page: Page): page.goto("/") page.get_by_test_id("Settings").click() @@ -41,7 +40,6 @@ ICON_TO_TOAST = { } -@pytest.mark.usefixtures("risk_accepted") def test_toast_positions(page: Page): page.goto("/") page.get_by_test_id("Settings").click() @@ -52,10 +50,10 @@ def test_toast_positions(page: Page): expect(page.locator(f"[{toast_selector}]")).to_be_visible() -@pytest.mark.usefixtures("risk_accepted") + def test_dark_mode(page: Page): page.goto("/") page.get_by_test_id("Settings").click() expect(page.locator("html")).not_to_have_attribute("class", "dark") page.locator("#switch-settings-theme-switch").click() - expect(page.locator("html")).to_have_attribute("class", "dark") + expect(page.locator("html")).to_have_attribute("class", "dark") \ No newline at end of file From 22599673ecc0f223ee4571079b8e44c87ef45746 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 Feb 2024 14:24:56 +0000 Subject: [PATCH 05/21] chore(trading): remove fixture usage (#5865) --- apps/trading/e2e/tests/deal_ticket/test_stop_order_oco.py | 2 +- apps/trading/e2e/tests/referrals/test_referrals.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/trading/e2e/tests/deal_ticket/test_stop_order_oco.py b/apps/trading/e2e/tests/deal_ticket/test_stop_order_oco.py index b4498001c..6d622ab0e 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_stop_order_oco.py +++ b/apps/trading/e2e/tests/deal_ticket/test_stop_order_oco.py @@ -235,7 +235,7 @@ def test_submit_stop_oco_market_order_pending( "PendingOCO" ) -@pytest.mark.usefixtures("page", "continuous_market", "auth", "risk_accepted") +@pytest.mark.usefixtures("auth", "risk_accepted") def test_submit_stop_oco_limit_order_pending( continuous_market, vega: VegaServiceNull, page: Page ): diff --git a/apps/trading/e2e/tests/referrals/test_referrals.py b/apps/trading/e2e/tests/referrals/test_referrals.py index 74dd00087..40601f439 100644 --- a/apps/trading/e2e/tests/referrals/test_referrals.py +++ b/apps/trading/e2e/tests/referrals/test_referrals.py @@ -119,7 +119,7 @@ def setup_market_and_referral_scheme(vega: VegaServiceNull, continuous_market: s forward_time(vega) -@pytest.mark.usefixtures("page", "auth", "risk_accepted") +@pytest.mark.usefixtures("auth", "risk_accepted") def test_can_traverse_up_and_down_through_tiers(continuous_market, vega: VegaServiceNull, page: Page): setup_market_and_referral_scheme(vega, continuous_market, page) change_keys(page, vega, PARTY_B.name) @@ -163,7 +163,7 @@ def test_can_traverse_up_and_down_through_tiers(continuous_market, vega: VegaSer "1%", "1", "1%", "0", "1", "1")) -@pytest.mark.usefixtures("page", "auth", "risk_accepted") +@pytest.mark.usefixtures("auth", "risk_accepted") def test_does_not_move_up_tiers_when_not_enough_epochs(continuous_market, vega: VegaServiceNull, page: Page): setup_market_and_referral_scheme(vega, continuous_market, page) change_keys(page, vega, PARTY_B.name) From 52e5a37da335835422eb1eab15b99d09644630d1 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 Feb 2024 15:52:22 +0000 Subject: [PATCH 06/21] chore(trading): fix skipped tests (#5868) --- ...test_trading_deal_ticket_submit_account.py | 22 +++-- .../e2e/tests/market/test_market_info.py | 3 - .../e2e/tests/orderbook/test_orderbook.py | 97 +++++++++---------- 3 files changed, 58 insertions(+), 64 deletions(-) diff --git a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py index 243456503..ba296b671 100644 --- a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py +++ b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py @@ -15,8 +15,7 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button" @pytest.fixture(scope="module") def vega(request): with init_vega(request) as vega_instance: - request.addfinalizer(lambda: cleanup_container( - vega_instance)) # Register the cleanup function + request.addfinalizer(lambda: cleanup_container(vega_instance)) yield vega_instance @@ -25,7 +24,6 @@ def continuous_market(vega): return setup_continuous_market(vega) -@pytest.mark.skip("tbd - issue only on the sim, should work in vega 0.74.0") @pytest.mark.usefixtures("auth", "risk_accepted") def test_should_display_info_and_button_for_deposit(continuous_market, page: Page): page.goto(f"/#/markets/{continuous_market}") @@ -33,17 +31,20 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag page.get_by_test_id(order_price).fill("20") # 7002-SORD-060 expect(page.get_by_test_id(deal_ticket_warning_margin)).to_have_text( - "You may not have enough margin available to open this position.") + "You may not have enough margin available to open this position." + ) page.get_by_test_id(deal_ticket_warning_margin).hover() expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text( - "1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI") + "1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI" + ) page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click() - expect(page.get_by_test_id("sidebar-content") - ).to_contain_text("DepositFrom") + expect(page.get_by_test_id("sidebar-content")).to_contain_text("DepositFrom") @pytest.mark.usefixtures("auth", "risk_accepted") -def test_should_show_an_error_if_your_balance_is_zero(continuous_market, vega: VegaServiceNull, page: Page): +def test_should_show_an_error_if_your_balance_is_zero( + continuous_market, vega: VegaServiceNull, page: Page +): page.goto(f"/#/markets/{continuous_market}") vega.create_key("key_empty") change_keys(page, vega, "key_empty") @@ -52,6 +53,7 @@ def test_should_show_an_error_if_your_balance_is_zero(continuous_market, vega: V # 7002-SORD-060 expect(page.get_by_test_id(place_order)).to_be_enabled() # 7002-SORD-003 - expect(page.get_by_test_id("deal-ticket-error-message-zero-balance") - ).to_have_text("You need tDAI in your wallet to trade in this market.Make a deposit") + expect(page.get_by_test_id("deal-ticket-error-message-zero-balance")).to_have_text( + "You need tDAI in your wallet to trade in this market.Make a deposit" + ) expect(page.get_by_test_id(deal_ticket_deposit_dialog_button)).to_be_visible() diff --git a/apps/trading/e2e/tests/market/test_market_info.py b/apps/trading/e2e/tests/market/test_market_info.py index fa9682226..287f2ebe2 100644 --- a/apps/trading/e2e/tests/market/test_market_info.py +++ b/apps/trading/e2e/tests/market/test_market_info.py @@ -107,7 +107,6 @@ def test_market_info_liquidation(page: Page): validate_info_section(page, fields) -@pytest.mark.skip("core issue #5681") def test_market_info_key_details(page: Page, vega: VegaServiceNull): # 6002-MDET-201 page.get_by_test_id(market_title_test_id).get_by_text( @@ -117,8 +116,6 @@ def test_market_info_key_details(page: Page, vega: VegaServiceNull): fields = [ ["Market ID", short_market_id], ["Name", "BTC:DAI_2023"], - ["Parent Market ID", "-"], - ["Insurance Pool Fraction", "-"], ["Status", "Active"], ["Trading Mode", "Continuous"], ["Market Decimal Places", "5"], diff --git a/apps/trading/e2e/tests/orderbook/test_orderbook.py b/apps/trading/e2e/tests/orderbook/test_orderbook.py index c1bd4827a..83654f490 100644 --- a/apps/trading/e2e/tests/orderbook/test_orderbook.py +++ b/apps/trading/e2e/tests/orderbook/test_orderbook.py @@ -1,10 +1,12 @@ import pytest from playwright.sync_api import Page, expect +from vega_sim.null_service import VegaServiceNull from typing import List from actions.vega import submit_order, submit_liquidity, submit_multiple_orders from conftest import init_vega, cleanup_container from fixtures.market import setup_simple_market from wallet_config import MM_WALLET, MM_WALLET2 +from actions.utils import next_epoch @pytest.fixture(scope="module") @@ -15,7 +17,7 @@ def vega(request): @pytest.fixture(scope="module") -def setup_market(vega): +def setup_market(vega: VegaServiceNull): market_id = setup_simple_market(vega) submit_liquidity(vega, MM_WALLET.name, market_id) submit_multiple_orders( @@ -33,10 +35,9 @@ def setup_market(vega): [[10, 69.995], [5, 70], [5, 85], [3, 90], [3, 95]], ) - vega.forward("10s") vega.wait_fn(1) vega.wait_for_total_catchup() - + return [ vega, market_id, @@ -46,19 +47,19 @@ def setup_market(vega): # these values don't align with the multiple orders above as # creating a trade triggers the liquidity provision orderbook_content = [ - [130.00500, 10, 94], - [130.00000, 3, 84], - [120.00000, 7, 81], - [110.00000, 5, 74], - [105.00000, 2, 69], - [101.00000, 67, 67], + [130.00500, 10, 126], + [130.00000, 3, 116], + [120.00000, 7, 113], + [110.00000, 5, 106], + [105.00000, 2, 101], + [101.00000, 99, 99], # mid - [99.00000, 102, 102], - [95.00000, 3, 105], - [90.00000, 3, 108], - [85.00000, 5, 113], - [70.00000, 5, 118], - [69.99500, 10, 128], + [99.00000, 99, 99], + [95.00000, 3, 102], + [90.00000, 3, 105], + [85.00000, 5, 110], + [70.00000, 5, 115], + [69.99500, 10, 125], ] @@ -81,7 +82,6 @@ def verify_prices_descending(page: Page): assert prices == sorted(prices, reverse=True) -@pytest.mark.skip("tbd") @pytest.mark.usefixtures("risk_accepted") def test_orderbook_grid_content(setup_market, page: Page): vega = setup_market[0] @@ -109,6 +109,7 @@ def test_orderbook_grid_content(setup_market, page: Page): vega.wait_fn(1) vega.wait_for_total_catchup() + next_epoch(vega) # 6003-ORDB-001 # 6003-ORDB-002 @@ -120,7 +121,6 @@ def test_orderbook_grid_content(setup_market, page: Page): page.goto(f"/#/markets/{market_id}") page.locator("[data-testid=Orderbook]").click() - # 6003-ORDB-013 assert ( float(page.locator("[data-testid*=last-traded]").text_content()) @@ -144,50 +144,46 @@ def test_orderbook_grid_content(setup_market, page: Page): def test_orderbook_resolution_change(setup_market, page: Page): market_id = setup_market[1] # 6003-ORDB-008 - orderbook_content_0_00 = [ - [130.01, 10, 94], - [130.00, 3, 84], - [120.00, 7, 81], - [110.00, 5, 74], - [105.00, 2, 69], - [101.00, 67, 67], + orderbook_content_0_01 = [ + [130.01, 10, 126], + [130.00, 3, 116], + [120.00, 7, 113], + [110.00, 5, 106], + [105.00, 2, 101], + [101.00, 99, 99], # mid - [99.00, 102, 102], - [95.00, 3, 105], - [90.00, 3, 108], - [85.00, 5, 113], - [70.00, 15, 128], + [99.00, 99, 99], + [95.00, 3, 102], + [90.00, 3, 105], + [85.00, 5, 110], + [70.00, 15, 125], ] - orderbook_content_10 = [ - [130, 13, 94], - [120, 7, 81], - [110, 7, 74], - [100, 67, 67], + orderbook_content_1 = [ + [130, 13, 126], + [120, 7, 113], + [110, 5, 106], + [105, 2, 101], + [101, 99, 99], # mid - [100, 105, 105], - [90, 8, 113], - [70, 15, 128], + [99, 99, 99], + [95, 3, 102], + [90, 3, 105], + [85, 5, 110], + [70, 15, 125], ] - orderbook_content_100 = [ - [100, 94, 94], - # mid - [100, 128, 128], - ] resolutions = [ - ["0.00", orderbook_content_0_00], - ["10", orderbook_content_10], - ["100", orderbook_content_100], + ["0.01", orderbook_content_0_01], + ["1", orderbook_content_1], ] page.goto(f"/#/markets/{market_id}") - # temporary skip - # for resolution in resolutions: - # page.get_by_test_id("resolution").click() - # page.get_by_role("menu").get_by_text(resolution[0], exact=True).click() - # verify_orderbook_grid(page, resolution[1]) + for resolution in resolutions: + page.get_by_test_id("resolution").click() + page.get_by_role("menuitem").get_by_text(resolution[0], exact=True).click() + verify_orderbook_grid(page, resolution[1]) @pytest.mark.usefixtures("risk_accepted") @@ -209,7 +205,6 @@ def test_orderbook_price_size_copy(setup_market, page: Page): expect(page.get_by_test_id("order-size")).to_have_value(volume.text_content()) -@pytest.mark.skip("tbd") @pytest.mark.usefixtures("risk_accepted") def test_orderbook_price_movement(setup_market, page: Page): vega = setup_market[0] From 4f8d6bd8766afa847cec16eb3acd725d34140b7d Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:17:08 +0200 Subject: [PATCH 07/21] fix(trading): banner for resume trading (#5849) Co-authored-by: Art Co-authored-by: Dariusz Majcherczyk --- .../market-banner/market-banner.tsx | 8 +- .../market-successor-proposal-banner.tsx | 6 +- .../market-update-banner.spec.tsx | 15 +--- .../market-banner/market-update-banner.tsx | 10 +-- .../market-update-state-banner.tsx | 81 ++++++++++++++----- .../market-banner/use-market-proposals.ts | 13 +-- .../market-header/mobile-market-header.tsx | 8 +- .../market_lifecycle/test_market_lifecycle.py | 32 +++++--- .../proposals-data-provider/Proposals.graphql | 39 ++++++++- .../__generated__/Proposals.ts | 49 +++++++++-- .../src/lib/proposals-data-provider/index.ts | 6 +- .../proposals-data-provider.tsx | 40 ++++++--- libs/types/src/__generated__/types.ts | 30 +++++-- 13 files changed, 245 insertions(+), 92 deletions(-) diff --git a/apps/trading/components/market-banner/market-banner.tsx b/apps/trading/components/market-banner/market-banner.tsx index ecea0c5f2..93f0d2869 100644 --- a/apps/trading/components/market-banner/market-banner.tsx +++ b/apps/trading/components/market-banner/market-banner.tsx @@ -7,7 +7,7 @@ import { useMaliciousOracle, } from '@vegaprotocol/markets'; import { useState } from 'react'; -import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; +import { type ProposalFragment } from '@vegaprotocol/proposals'; import { MarketSuspendedBanner } from './market-suspended-banner'; import { MarketUpdateBanner } from './market-update-banner'; import { MarketUpdateStateBanner } from './market-update-state-banner'; @@ -22,17 +22,17 @@ import { type UpdateMarketBanner = { kind: 'UpdateMarket'; - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }; type UpdateMarketStateBanner = { kind: 'UpdateMarketState'; - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }; type NewMarketBanner = { kind: 'NewMarket'; // aka a proposal of NewMarket which succeeds the current market - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }; type SettledBanner = { diff --git a/apps/trading/components/market-banner/market-successor-proposal-banner.tsx b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx index ed58174da..07864c4a8 100644 --- a/apps/trading/components/market-banner/market-successor-proposal-banner.tsx +++ b/apps/trading/components/market-banner/market-successor-proposal-banner.tsx @@ -1,5 +1,5 @@ import { Fragment } from 'react'; -import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; +import { type ProposalFragment } from '@vegaprotocol/proposals'; import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import { useT } from '../../lib/use-t'; @@ -7,7 +7,7 @@ import { useT } from '../../lib/use-t'; export const MarketSuccessorProposalBanner = ({ proposals, }: { - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }) => { const t = useT(); const tokenLink = useLinks(DApp.Governance); @@ -28,7 +28,7 @@ export const MarketSuccessorProposalBanner = ({ } )}{' '} {proposals.map((item, i) => { - if (item.terms.change.__typename !== 'NewMarket') { + if (item.terms?.change.__typename !== 'NewMarket') { return null; } diff --git a/apps/trading/components/market-banner/market-update-banner.spec.tsx b/apps/trading/components/market-banner/market-update-banner.spec.tsx index 6113ebc50..db217ad3f 100644 --- a/apps/trading/components/market-banner/market-update-banner.spec.tsx +++ b/apps/trading/components/market-banner/market-update-banner.spec.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { ProposalState } from '@vegaprotocol/types'; import { MarketUpdateBanner } from './market-update-banner'; -import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; +import { type ProposalFragment } from '@vegaprotocol/proposals'; describe('MarketUpdateBanner', () => { const change = { @@ -34,9 +34,7 @@ describe('MarketUpdateBanner', () => { it('renders content for a single open proposal', () => { render( - + ); expect( @@ -50,9 +48,7 @@ describe('MarketUpdateBanner', () => { it('renders content for a single passed proposal', () => { render( - + ); expect( @@ -65,10 +61,7 @@ describe('MarketUpdateBanner', () => { }); it('renders content for multiple passed proposals', () => { - const proposals = [ - openProposal, - openProposal, - ] as MarketViewProposalFieldsFragment[]; + const proposals = [openProposal, openProposal] as ProposalFragment[]; render(); diff --git a/apps/trading/components/market-banner/market-update-banner.tsx b/apps/trading/components/market-banner/market-update-banner.tsx index 352dd6d31..6dd4cfd8f 100644 --- a/apps/trading/components/market-banner/market-update-banner.tsx +++ b/apps/trading/components/market-banner/market-update-banner.tsx @@ -6,7 +6,7 @@ import { TOKEN_PROPOSALS, useLinks, } from '@vegaprotocol/environment'; -import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; +import { type ProposalFragment } from '@vegaprotocol/proposals'; import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ProposalState } from '@vegaprotocol/types'; import { useT } from '../../lib/use-t'; @@ -14,17 +14,17 @@ import { useT } from '../../lib/use-t'; export const MarketUpdateBanner = ({ proposals, }: { - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }) => { const governanceLink = useLinks(DApp.Governance); const t = useT(); const openProposals = sortBy( proposals.filter((p) => p.state === ProposalState.STATE_OPEN), - (p) => p.terms.enactmentDatetime + (p) => p.terms?.enactmentDatetime ); const passedProposals = sortBy( proposals.filter((p) => p.state === ProposalState.STATE_PASSED), - (p) => p.terms.enactmentDatetime + (p) => p.terms?.enactmentDatetime ); let content = null; @@ -49,7 +49,7 @@ export const MarketUpdateBanner = ({ content = (

    {t('Proposal set to change market on {{date}}.', { - date: format(new Date(proposal.terms.enactmentDatetime), 'dd MMMM'), + date: format(new Date(proposal.terms?.enactmentDatetime), 'dd MMMM'), })} {t('View proposal')},

    diff --git a/apps/trading/components/market-banner/market-update-state-banner.tsx b/apps/trading/components/market-banner/market-update-state-banner.tsx index bb85b3831..5e2beba93 100644 --- a/apps/trading/components/market-banner/market-update-state-banner.tsx +++ b/apps/trading/components/market-banner/market-update-state-banner.tsx @@ -1,8 +1,8 @@ import { type ReactNode } from 'react'; import sortBy from 'lodash/sortBy'; import { format, formatDuration, intervalToDuration } from 'date-fns'; -import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals'; -import { ProposalState } from '@vegaprotocol/types'; +import { type ProposalFragment } from '@vegaprotocol/proposals'; +import { MarketUpdateType, ProposalState } from '@vegaprotocol/types'; import { DApp, TOKEN_PROPOSAL, @@ -19,18 +19,29 @@ export const MarketUpdateStateBanner = ({ proposals, }: { market: Market; - proposals: MarketViewProposalFieldsFragment[]; + proposals: ProposalFragment[]; }) => { const t = useT(); const governanceLink = useLinks(DApp.Governance); + const openTradingProposals = sortBy( + proposals.filter( + (p) => + p.terms && + p.terms.change.__typename === 'UpdateMarketState' && + p.terms.change.updateType === + MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME + ), + (p) => p.terms?.enactmentDatetime + ); + const openProposals = sortBy( proposals.filter((p) => p.state === ProposalState.STATE_OPEN), - (p) => p.terms.enactmentDatetime + (p) => p.terms?.enactmentDatetime ); const passedProposals = sortBy( proposals.filter((p) => p.state === ProposalState.STATE_PASSED), - (p) => p.terms.enactmentDatetime + (p) => p.terms?.enactmentDatetime ); if (!passedProposals.length && !openProposals.length) { @@ -45,12 +56,41 @@ export const MarketUpdateStateBanner = ({ ? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id)) : undefined; + const openTradingProposalsLink = + openTradingProposals[0]?.__typename === 'Proposal' && + openTradingProposals[0]?.id + ? governanceLink( + TOKEN_PROPOSAL.replace(':id', openTradingProposals[0].id) + ) + : openTradingProposals[0]?.__typename === 'ProposalDetail' && + openTradingProposals[0]?.batchId + ? governanceLink( + TOKEN_PROPOSAL.replace(':id', openTradingProposals[0].batchId) + ) + : undefined; + const proposalsLink = openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined; let content: ReactNode; - if (passedProposals.length) { + if (openTradingProposals.length >= 1) { + content = ( + <> +

    + {t( + 'Trading on market {{name}} was suspended by governance. There are open proposals to resume trading on this market.', + { name } + )} +

    +

    + + {t('View proposals')} + +

    + + ); + } else if (passedProposals.length) { const { date, duration, price } = getMessageVariables(passedProposals[0]); content = ( <> @@ -116,20 +156,23 @@ export const MarketUpdateStateBanner = ({ return
    {content}
    ; }; -const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => { - const enactmentDatetime = new Date(proposal.terms.enactmentDatetime); - const date = format(enactmentDatetime, 'dd MMMM'); - const duration = formatDuration( - intervalToDuration({ - start: new Date(), - end: enactmentDatetime, - }), - { - format: ['days', 'hours'], - } - ); +const getMessageVariables = (proposal: ProposalFragment) => { + const enactmentDatetime = + proposal.terms && new Date(proposal.terms.enactmentDatetime); + const date = enactmentDatetime && format(enactmentDatetime, 'dd MMMM'); + const duration = + enactmentDatetime && + formatDuration( + intervalToDuration({ + start: new Date(), + end: enactmentDatetime, + }), + { + format: ['days', 'hours'], + } + ); const price = - proposal.terms.change.__typename === 'UpdateMarketState' + proposal.terms?.change.__typename === 'UpdateMarketState' ? proposal.terms.change.price : ''; return { diff --git a/apps/trading/components/market-banner/use-market-proposals.ts b/apps/trading/components/market-banner/use-market-proposals.ts index 0583a9238..2a2b62685 100644 --- a/apps/trading/components/market-banner/use-market-proposals.ts +++ b/apps/trading/components/market-banner/use-market-proposals.ts @@ -1,10 +1,10 @@ import { useMarketProposals, - type MarketViewProposalFieldsFragment, + type ProposalFragment, } from '@vegaprotocol/proposals'; import { ProposalState, ProposalType } from '@vegaprotocol/types'; -const isPending = (p: MarketViewProposalFieldsFragment) => { +const isPending = (p: ProposalFragment) => { return [ProposalState.STATE_OPEN, ProposalState.STATE_PASSED].includes( p.state ); @@ -21,9 +21,10 @@ export const useUpdateMarketStateProposals = ( const proposals = data ? data.filter(isPending).filter((p) => { - const change = p.terms.change; + const change = p.terms?.change; if ( + change && change.__typename === 'UpdateMarketState' && change.market.id === marketId ) { @@ -48,8 +49,9 @@ export const useUpdateMarketProposals = ( const proposals = data ? data.filter(isPending).filter((p) => { - const change = p.terms.change; + const change = p.terms?.change; if ( + change && change.__typename === 'UpdateMarket' && change.marketId === marketId ) { @@ -73,8 +75,9 @@ export const useSuccessorMarketProposals = ( const proposals = data ? data.filter(isPending).filter((p) => { - const change = p.terms.change; + const change = p.terms?.change; if ( + change && change.__typename === 'NewMarket' && change.successorConfiguration?.parentMarketId === marketId ) { diff --git a/apps/trading/components/market-header/mobile-market-header.tsx b/apps/trading/components/market-header/mobile-market-header.tsx index 0fd0c08e6..250ade3ad 100644 --- a/apps/trading/components/market-header/mobile-market-header.tsx +++ b/apps/trading/components/market-header/mobile-market-header.tsx @@ -12,6 +12,7 @@ import { useT } from '../../lib/use-t'; import classNames from 'classnames'; import { MarketHeaderStats } from '../../client-pages/market/market-header-stats'; import { MarketMarkPrice } from '../market-mark-price'; +import { MarketBanner } from '../market-banner'; /** * This is only rendered for the mobile navigation */ @@ -108,8 +109,11 @@ export const MobileMarketHeader = () => { } > {data && ( -
    - +
    + +
    + +
    )} diff --git a/apps/trading/e2e/tests/market_lifecycle/test_market_lifecycle.py b/apps/trading/e2e/tests/market_lifecycle/test_market_lifecycle.py index dc2532e0b..788ca63c8 100644 --- a/apps/trading/e2e/tests/market_lifecycle/test_market_lifecycle.py +++ b/apps/trading/e2e/tests/market_lifecycle/test_market_lifecycle.py @@ -9,6 +9,7 @@ from actions.utils import next_epoch from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET from vega_sim.api import governance + @pytest.mark.usefixtures("risk_accepted") def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page): # 7002-SORD-001 @@ -16,15 +17,18 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page): trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id( "item-value" ) - market_state = page.get_by_test_id("market-state").get_by_test_id("item-value") + market_state = page.get_by_test_id( + "market-state").get_by_test_id("item-value") # setup market in proposed step, without liquidity provided market_id = proposed_market page.goto(f"/#/markets/{market_id}") # 6002-MDET-001 - expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr") + expect(page.get_by_test_id("header-title") + ).to_have_text("BTC:DAI_2023Futr") # 6002-MDET-002 - expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based") + expect(page.get_by_test_id("market-expiry") + ).to_have_text("ExpiryNot time-based") page.get_by_test_id("market-expiry").hover() expect(page.get_by_test_id("expiry-tooltip").first).to_have_text( "This market expires when triggered by its oracle, not on a set date.View oracle specification" @@ -123,16 +127,16 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page): expect(market_state).to_have_text("Active") vega.update_market_state( - market_id = market_id, - proposal_key = MM_WALLET.name, - market_state = MarketStateUpdateType.Suspend, - forward_time_to_enactment = False + market_id=market_id, + proposal_key=MM_WALLET.name, + market_state=MarketStateUpdateType.Suspend, + forward_time_to_enactment=False ) expect( page - .get_by_test_id("market-banner") - .get_by_test_id(f"update-state-banner-{market_id}") + .get_by_test_id("market-banner") + .get_by_test_id(f"update-state-banner-{market_id}") ).to_be_visible() next_epoch(vega=vega) @@ -143,12 +147,14 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page): # banner should not show after resume vega.update_market_state( - market_id = market_id, - proposal_key = MM_WALLET.name, - market_state = MarketStateUpdateType.Resume, - forward_time_to_enactment = False + market_id=market_id, + proposal_key=MM_WALLET.name, + market_state=MarketStateUpdateType.Resume, + forward_time_to_enactment=False ) + expect(page.get_by_test_id("market-banner") + ).to_have_text(" Trading on market BTC:DAI_2023 was suspended by governance. There are open proposals to resume trading on this market.View proposals1/2") next_epoch(vega=vega) expect(page.get_by_test_id("market-banner")).not_to_be_visible() diff --git a/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql b/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql index 1b32478a4..2169b7de0 100644 --- a/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql +++ b/libs/proposals/src/lib/proposals-data-provider/Proposals.graphql @@ -485,14 +485,45 @@ fragment MarketViewProposalFields on Proposal { } } +fragment SubProposal on ProposalDetail { + id + batchId + reference + state + terms { + closingDatetime + enactmentDatetime + change { + __typename + ... on UpdateMarketState { + ...UpdateMarketStateFields + } + ... on NewMarket { + ...NewMarketSuccessorFields + } + ... on UpdateMarket { + ...UpdateMarketFields + } + } + } +} + query MarketViewProposals( $proposalType: ProposalType $inState: ProposalState ) { proposalsConnection(proposalType: $proposalType, inState: $inState) { edges { - node { - ...MarketViewProposalFields + proposalNode { + ... on BatchProposal { + id + subProposals { + ...SubProposal + } + } + ... on Proposal { + ...MarketViewProposalFields + } } } } @@ -500,6 +531,8 @@ query MarketViewProposals( subscription MarketViewLiveProposals { proposals { - ...MarketViewProposalFields + ... on Proposal { + ...MarketViewProposalFields + } } } diff --git a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts index af09b328e..9f728fbd3 100644 --- a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts +++ b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts @@ -33,13 +33,15 @@ export type NewMarketSuccessorFieldsFragment = { __typename?: 'NewMarket', instr export type MarketViewProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset' } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename: 'NewSpotMarket' } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset' } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | { __typename?: 'UpdatePerpetualProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecPerpetualBinding', settlementDataProperty: string, settlementScheduleProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } } | { __typename: 'UpdateNetworkParameter' } | { __typename: 'UpdateReferralProgram' } | { __typename: 'UpdateSpotMarket' } | { __typename: 'UpdateVolumeDiscountProgram' } } }; +export type SubProposalFragment = { __typename?: 'ProposalDetail', id?: string | null, batchId?: string | null, reference: string, state: Types.ProposalState, terms?: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset' } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename: 'NewSpotMarket' } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset' } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | { __typename?: 'UpdatePerpetualProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecPerpetualBinding', settlementDataProperty: string, settlementScheduleProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } } | { __typename: 'UpdateNetworkParameter' } | { __typename: 'UpdateReferralProgram' } | { __typename: 'UpdateSpotMarket' } | { __typename: 'UpdateVolumeDiscountProgram' } } | null }; + export type MarketViewProposalsQueryVariables = Types.Exact<{ proposalType?: Types.InputMaybe; inState?: Types.InputMaybe; }>; -export type MarketViewProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset' } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename: 'NewSpotMarket' } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset' } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | { __typename?: 'UpdatePerpetualProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecPerpetualBinding', settlementDataProperty: string, settlementScheduleProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } } | { __typename: 'UpdateNetworkParameter' } | { __typename: 'UpdateReferralProgram' } | { __typename: 'UpdateSpotMarket' } | { __typename: 'UpdateVolumeDiscountProgram' } } } } | null> | null } | null }; +export type MarketViewProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', proposalNode?: { __typename?: 'BatchProposal', id?: string | null, subProposals?: Array<{ __typename?: 'ProposalDetail', id?: string | null, batchId?: string | null, reference: string, state: Types.ProposalState, terms?: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset' } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename: 'NewSpotMarket' } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset' } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | { __typename?: 'UpdatePerpetualProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecPerpetualBinding', settlementDataProperty: string, settlementScheduleProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } } | { __typename: 'UpdateNetworkParameter' } | { __typename: 'UpdateReferralProgram' } | { __typename: 'UpdateSpotMarket' } | { __typename: 'UpdateVolumeDiscountProgram' } } | null } | null> | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'CancelTransfer' } | { __typename: 'NewAsset' } | { __typename: 'NewFreeform' } | { __typename: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string }, successorConfiguration?: { __typename?: 'SuccessorConfiguration', parentMarketId: string } | null } | { __typename: 'NewSpotMarket' } | { __typename: 'NewTransfer' } | { __typename: 'UpdateAsset' } | { __typename: 'UpdateMarket', marketId: string, updateMarketConfiguration: { __typename?: 'UpdateMarketConfiguration', metadata?: Array | null, instrument: { __typename?: 'UpdateInstrumentConfiguration', code: string, product: { __typename?: 'UpdateFutureProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } | { __typename?: 'UpdatePerpetualProduct', quoteName: string, dataSourceSpecForSettlementData: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecForSettlementSchedule: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null }> | null }> | null } | { __typename?: 'EthCallSpec' } } | { __typename?: 'DataSourceDefinitionInternal' } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecPerpetualBinding', settlementDataProperty: string, settlementScheduleProperty: string } } }, priceMonitoringParameters: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, riskParameters: { __typename: 'UpdateMarketLogNormalRiskModel', logNormal?: { __typename?: 'LogNormalRiskModel', riskAversionParameter: number, tau: number, params: { __typename?: 'LogNormalModelParams', mu: number, r: number, sigma: number } } | null } | { __typename: 'UpdateMarketSimpleRiskModel', simple?: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } | null } } } | { __typename: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } } | { __typename: 'UpdateNetworkParameter' } | { __typename: 'UpdateReferralProgram' } | { __typename: 'UpdateSpotMarket' } | { __typename: 'UpdateVolumeDiscountProgram' } } } | null } | null> | null } | null }; export type MarketViewLiveProposalsSubscriptionVariables = Types.Exact<{ [key: string]: never; }>; @@ -525,6 +527,32 @@ export const MarketViewProposalFieldsFragmentDoc = gql` ${UpdateMarketStateFieldsFragmentDoc} ${NewMarketSuccessorFieldsFragmentDoc} ${UpdateMarketFieldsFragmentDoc}`; +export const SubProposalFragmentDoc = gql` + fragment SubProposal on ProposalDetail { + id + batchId + reference + state + terms { + closingDatetime + enactmentDatetime + change { + __typename + ... on UpdateMarketState { + ...UpdateMarketStateFields + } + ... on NewMarket { + ...NewMarketSuccessorFields + } + ... on UpdateMarket { + ...UpdateMarketFields + } + } + } +} + ${UpdateMarketStateFieldsFragmentDoc} +${NewMarketSuccessorFieldsFragmentDoc} +${UpdateMarketFieldsFragmentDoc}`; export const ProposalsListDocument = gql` query ProposalsList($proposalType: ProposalType, $inState: ProposalState) { proposalsConnection(proposalType: $proposalType, inState: $inState) { @@ -569,13 +597,22 @@ export const MarketViewProposalsDocument = gql` query MarketViewProposals($proposalType: ProposalType, $inState: ProposalState) { proposalsConnection(proposalType: $proposalType, inState: $inState) { edges { - node { - ...MarketViewProposalFields + proposalNode { + ... on BatchProposal { + id + subProposals { + ...SubProposal + } + } + ... on Proposal { + ...MarketViewProposalFields + } } } } } - ${MarketViewProposalFieldsFragmentDoc}`; + ${SubProposalFragmentDoc} +${MarketViewProposalFieldsFragmentDoc}`; /** * __useMarketViewProposalsQuery__ @@ -608,7 +645,9 @@ export type MarketViewProposalsQueryResult = Apollo.QueryResult; const getData = (responseData: ProposalsListQuery | null) => responseData?.proposalsConnection?.edges @@ -63,23 +68,23 @@ const ProposalTypeMap: Record< }; const matchFilter = ( - data: MarketViewProposalFieldsFragment, + data: ProposalFragment, variables: MarketViewProposalsQueryVariables ) => { return ( (!variables.inState || data.state === variables.inState) && (!variables.proposalType || - data.terms.change.__typename === ProposalTypeMap[variables.proposalType]) + data.terms?.change.__typename === ProposalTypeMap[variables.proposalType]) ); }; export const update: Update< - MarketViewProposalFieldsFragment[] | null, - MarketViewProposalFieldsFragment, + ProposalFragment[] | null, + ProposalFragment, MarketViewProposalsQueryVariables > = ( - data: MarketViewProposalFieldsFragment[] | null, - delta: MarketViewProposalFieldsFragment, + data: ProposalFragment[] | null, + delta: ProposalFragment, reload, variables ) => { @@ -104,13 +109,30 @@ export const update: Update< const getMarketProposalsData = ( responseData: MarketViewProposalsQuery | null -) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || []; +): ProposalFragments => { + const proposals: ProposalFragment[] = []; + responseData?.proposalsConnection?.edges?.forEach((edge) => { + if (edge?.proposalNode) { + if (edge.proposalNode.__typename === 'Proposal') { + proposals.push(edge.proposalNode); + } else if ( + edge.proposalNode.__typename === 'BatchProposal' && + edge.proposalNode.subProposals + ) { + proposals.push( + ...(edge.proposalNode.subProposals as ProposalFragments) + ); + } + } + }); + return proposals; +}; const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {}; export const marketViewProposalsDataProvider = makeDataProvider< MarketViewProposalsQuery, - MarketViewProposalFieldsFragment[], + ProposalFragments, MarketViewLiveProposalsSubscription, MarketViewProposalFieldsFragment, MarketViewProposalsQueryVariables, diff --git a/libs/types/src/__generated__/types.ts b/libs/types/src/__generated__/types.ts index ce15089ec..ec9c650ef 100644 --- a/libs/types/src/__generated__/types.ts +++ b/libs/types/src/__generated__/types.ts @@ -1232,6 +1232,14 @@ export type EpochTimestamps = { start?: Maybe; }; +export type EquityLikeShareWeightPerMarket = { + __typename?: 'EquityLikeShareWeightPerMarket'; + /** The equity-like share weight for this market */ + equityLikeShareWeight: Scalars['String']; + /** The market ID */ + marketId: Scalars['String']; +}; + /** Response for the signature bundle to allowlist an ERC20 token in the collateral bridge */ export type Erc20ListAssetBundle = { __typename?: 'Erc20ListAssetBundle'; @@ -1955,7 +1963,7 @@ export type LiquidityOrderReference = { /** Information about a liquidity provider */ export type LiquidityProvider = { __typename?: 'LiquidityProvider'; - /** Information used for calculating an LP's fee share, such as the equity like share, average entry valuation and liquidity score, for the given liquidity provider and market */ + /** Information used for calculating an LP's fee share, such as the equity-like share, average entry valuation and liquidity score, for the given liquidity provider and market */ feeShare?: Maybe; /** Market ID the liquidity provision is for */ marketId: Scalars['ID']; @@ -1983,7 +1991,7 @@ export type LiquidityProviderEdge = { node: LiquidityProvider; }; -/** The equity like share of liquidity fee for each liquidity provider */ +/** The equity-like share of liquidity fee for each liquidity provider */ export type LiquidityProviderFeeShare = { __typename?: 'LiquidityProviderFeeShare'; /** The average entry valuation of the liquidity provider for the market */ @@ -2483,7 +2491,7 @@ export type MarketData = { indicativeVolume: Scalars['String']; /** The last traded price (an unsigned integer) */ lastTradedPrice: Scalars['String']; - /** The equity like share of liquidity fee for each liquidity provider */ + /** The equity-like share of liquidity fee for each liquidity provider */ liquidityProviderFeeShare?: Maybe>; /** SLA performance statistics */ liquidityProviderSla?: Maybe>; @@ -3032,7 +3040,7 @@ export type Normaliser = { name: Scalars['String']; }; -/** The equity like share of liquidity fee for each liquidity provider */ +/** The equity-like share of liquidity fee for each liquidity provider */ export type ObservableLiquidityProviderFeeShare = { __typename?: 'ObservableLiquidityProviderFeeShare'; /** The average entry valuation of the liquidity provider for the market */ @@ -3099,7 +3107,7 @@ export type ObservableMarketData = { indicativeVolume: Scalars['String']; /** The last traded price (an unsigned integer) */ lastTradedPrice: Scalars['String']; - /** The equity like share of liquidity fee for each liquidity provider */ + /** The equity-like share of liquidity fee for each liquidity provider */ liquidityProviderFeeShare?: Maybe>; /** SLA performance statistics */ liquidityProviderSla?: Maybe>; @@ -3830,12 +3838,14 @@ export type PartytradesConnectionArgs = { /** Represents a party on Vega, could be an ethereum wallet address in the future */ export type PartytransfersConnectionArgs = { direction?: InputMaybe; + fromAccountType?: InputMaybe; fromEpoch?: InputMaybe; gameId?: InputMaybe; isReward?: InputMaybe; pagination?: InputMaybe; scope?: InputMaybe; status?: InputMaybe; + toAccountType?: InputMaybe; toEpoch?: InputMaybe; }; @@ -4471,7 +4481,7 @@ export enum ProposalRejectionReason { PROPOSAL_ERROR_GOVERNANCE_TRANSFER_PROPOSAL_INVALID = 'PROPOSAL_ERROR_GOVERNANCE_TRANSFER_PROPOSAL_INVALID', /** Proposal terms timestamps are not compatible (Validation < Closing < Enactment) */ PROPOSAL_ERROR_INCOMPATIBLE_TIMESTAMPS = 'PROPOSAL_ERROR_INCOMPATIBLE_TIMESTAMPS', - /** The proposal is rejected because the party does not have enough equity like share in the market */ + /** The proposal is rejected because the party does not have enough equity-like share in the market */ PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE = 'PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE', /** The proposer for this proposal has insufficient tokens */ PROPOSAL_ERROR_INSUFFICIENT_TOKENS = 'PROPOSAL_ERROR_INSUFFICIENT_TOKENS', @@ -4653,7 +4663,7 @@ export type ProposalVoteEdge = { export type ProposalVoteSide = { __typename?: 'ProposalVoteSide'; - /** Total equity like share weight for this side (only for UpdateMarket Proposals) */ + /** Total equity-like share weight for this side (only for UpdateMarket Proposals) */ totalEquityLikeShareWeight: Scalars['String']; /** Total number of votes cast for this side */ totalNumber: Scalars['String']; @@ -5466,6 +5476,7 @@ export type QuerytransferArgs = { /** Queries allow a caller to read data and filter data via GraphQL. */ export type QuerytransfersConnectionArgs = { direction?: InputMaybe; + fromAccountType?: InputMaybe; fromEpoch?: InputMaybe; gameId?: InputMaybe; isReward?: InputMaybe; @@ -5473,6 +5484,7 @@ export type QuerytransfersConnectionArgs = { partyId?: InputMaybe; scope?: InputMaybe; status?: InputMaybe; + toAccountType?: InputMaybe; toEpoch?: InputMaybe; }; @@ -7238,7 +7250,9 @@ export type Vote = { __typename?: 'Vote'; /** RFC3339Nano time and date when the vote reached Vega network */ datetime: Scalars['Timestamp']; - /** The weight of this vote based on the total equity like share */ + /** The equity-like share weight per market (only for batch proposals) */ + equityLikeSharePerMarket?: Maybe>>; + /** The weight of this vote based on the total equity-like share */ equityLikeShareWeight: Scalars['String']; /** Total number of governance tokens for the party that cast the vote */ governanceTokenBalance: Scalars['String']; From b81c4bc9480a0b5c0e4d0fa2d194dee0358afe44 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 27 Feb 2024 11:46:48 +0100 Subject: [PATCH 08/21] fix(governance): add multisig score to the validator page (#5845) Co-authored-by: Dariusz Majcherczyk --- .../src/lib/get-multisig-status-info.spec.ts | 27 +++++++++++ .../src/lib/get-multisig-status-info.ts | 21 ++++++--- .../src/routes/staking/PreviousEpoch.graphql | 40 ++++++++-------- .../staking/__generated__/PreviousEpoch.ts | 46 +++++++++++-------- .../routes/staking/node/validator-table.tsx | 33 +++++++++++++ libs/i18n/src/locales/en/governance.json | 2 + 6 files changed, 124 insertions(+), 45 deletions(-) diff --git a/apps/governance/src/lib/get-multisig-status-info.spec.ts b/apps/governance/src/lib/get-multisig-status-info.spec.ts index d92d5a7d5..8e3112e4b 100644 --- a/apps/governance/src/lib/get-multisig-status-info.spec.ts +++ b/apps/governance/src/lib/get-multisig-status-info.spec.ts @@ -20,6 +20,7 @@ describe('getMultisigStatus', () => { expect(result).toEqual({ multisigStatus: MultisigStatus.noNodes, showMultisigStatusError: true, + zeroScoreNodes: [], }); }); @@ -35,6 +36,7 @@ describe('getMultisigStatus', () => { expect(result).toEqual({ multisigStatus: MultisigStatus.correct, showMultisigStatusError: false, + zeroScoreNodes: [], }); }); @@ -50,6 +52,22 @@ describe('getMultisigStatus', () => { expect(result).toEqual({ multisigStatus: MultisigStatus.nodeNeedsRemoving, showMultisigStatusError: true, + zeroScoreNodes: [ + { + id: '1', + rewardScore: { + multisigScore: '0', + }, + stakedTotal: '1000', + }, + { + id: '2', + rewardScore: { + multisigScore: '0', + }, + stakedTotal: '1000', + }, + ], }); }); @@ -65,6 +83,15 @@ describe('getMultisigStatus', () => { expect(result).toEqual({ multisigStatus: MultisigStatus.nodeNeedsAdding, showMultisigStatusError: true, + zeroScoreNodes: [ + { + id: '1', + rewardScore: { + multisigScore: '0', + }, + stakedTotal: '1000', + }, + ], }); }); }); diff --git a/apps/governance/src/lib/get-multisig-status-info.ts b/apps/governance/src/lib/get-multisig-status-info.ts index 53352c6bf..608ade107 100644 --- a/apps/governance/src/lib/get-multisig-status-info.ts +++ b/apps/governance/src/lib/get-multisig-status-info.ts @@ -1,5 +1,8 @@ import { removePaginationWrapper } from '@vegaprotocol/utils'; -import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch'; +import type { + PreviousEpochQuery, + ValidatorNodeFragment, +} from '../routes/staking/__generated__/PreviousEpoch'; export enum MultisigStatus { 'correct' = 'correct', @@ -17,12 +20,15 @@ export const getMultisigStatusInfo = ( previousEpochData?.epoch.validatorsConnection?.edges ); - const hasZero = allNodesInPreviousEpoch.some( - (node) => Number(node?.rewardScore?.multisigScore) === 0 - ); - const hasOne = allNodesInPreviousEpoch.some( - (node) => Number(node?.rewardScore?.multisigScore) === 1 - ); + const zeroScore = (node: ValidatorNodeFragment) => + Number(node.rewardScore?.multisigScore) === 0; + const oneScore = (node: ValidatorNodeFragment) => + Number(node.rewardScore?.multisigScore) === 1; + + const hasZero = allNodesInPreviousEpoch.some(zeroScore); + const hasOne = allNodesInPreviousEpoch.some(oneScore); + + const zeroScoreNodes = allNodesInPreviousEpoch.filter(zeroScore); if (hasZero && hasOne) { // If any individual node has 0 it means that node is missing from the multisig and needs to be added @@ -38,5 +44,6 @@ export const getMultisigStatusInfo = ( return { showMultisigStatusError: status !== MultisigStatus.correct, multisigStatus: status, + zeroScoreNodes, }; }; diff --git a/apps/governance/src/routes/staking/PreviousEpoch.graphql b/apps/governance/src/routes/staking/PreviousEpoch.graphql index 1b6c42e03..d064e3d8e 100644 --- a/apps/governance/src/routes/staking/PreviousEpoch.graphql +++ b/apps/governance/src/routes/staking/PreviousEpoch.graphql @@ -1,27 +1,31 @@ +fragment ValidatorNode on Node { + id + stakedTotal + rewardScore { + rawValidatorScore + performanceScore + multisigScore + validatorScore + normalisedScore + validatorStatus + } + rankingScore { + status + previousStatus + rankingScore + stakeScore + performanceScore + votingPower + } +} + query PreviousEpoch($epochId: ID) { epoch(id: $epochId) { id validatorsConnection { edges { node { - id - stakedTotal - rewardScore { - rawValidatorScore - performanceScore - multisigScore - validatorScore - normalisedScore - validatorStatus - } - rankingScore { - status - previousStatus - rankingScore - stakeScore - performanceScore - votingPower - } + ...ValidatorNode } } } diff --git a/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts b/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts index 002e75b5c..4246b5248 100644 --- a/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts +++ b/apps/governance/src/routes/staking/__generated__/PreviousEpoch.ts @@ -3,6 +3,8 @@ import * as Types from '@vegaprotocol/types'; import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; +export type ValidatorNodeFragment = { __typename?: 'Node', id: string, stakedTotal: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string, multisigScore: string, validatorScore: string, normalisedScore: string, validatorStatus: Types.ValidatorStatus } | null, rankingScore: { __typename?: 'RankingScore', status: Types.ValidatorStatus, previousStatus: Types.ValidatorStatus, rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string } }; + export type PreviousEpochQueryVariables = Types.Exact<{ epochId?: Types.InputMaybe; }>; @@ -10,7 +12,28 @@ export type PreviousEpochQueryVariables = Types.Exact<{ export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, stakedTotal: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string, multisigScore: string, validatorScore: string, normalisedScore: string, validatorStatus: Types.ValidatorStatus } | null, rankingScore: { __typename?: 'RankingScore', status: Types.ValidatorStatus, previousStatus: Types.ValidatorStatus, rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string } } } | null> | null } | null } }; - +export const ValidatorNodeFragmentDoc = gql` + fragment ValidatorNode on Node { + id + stakedTotal + rewardScore { + rawValidatorScore + performanceScore + multisigScore + validatorScore + normalisedScore + validatorStatus + } + rankingScore { + status + previousStatus + rankingScore + stakeScore + performanceScore + votingPower + } +} + `; export const PreviousEpochDocument = gql` query PreviousEpoch($epochId: ID) { epoch(id: $epochId) { @@ -18,30 +41,13 @@ export const PreviousEpochDocument = gql` validatorsConnection { edges { node { - id - stakedTotal - rewardScore { - rawValidatorScore - performanceScore - multisigScore - validatorScore - normalisedScore - validatorStatus - } - rankingScore { - status - previousStatus - rankingScore - stakeScore - performanceScore - votingPower - } + ...ValidatorNode } } } } } - `; + ${ValidatorNodeFragmentDoc}`; /** * __usePreviousEpochQuery__ diff --git a/apps/governance/src/routes/staking/node/validator-table.tsx b/apps/governance/src/routes/staking/node/validator-table.tsx index 8e302a019..7c0c79a19 100644 --- a/apps/governance/src/routes/staking/node/validator-table.tsx +++ b/apps/governance/src/routes/staking/node/validator-table.tsx @@ -37,6 +37,7 @@ 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', @@ -104,6 +105,10 @@ export const ValidatorTable = ({ }; }, [node, previousEpochData?.epoch.validatorsConnection?.edges]); + const multisigStatus = previousEpochData + ? getMultisigStatusInfo(previousEpochData) + : undefined; + return ( <>

    @@ -281,6 +286,34 @@ export const ValidatorTable = ({ + + {t('multisigPenalty')} + + + {multisigStatus?.zeroScoreNodes.find( + (n) => n.id === node.id + ) ? ( + + + + ) : null} + + + {formatNumberPercentage( + BigNumber( + multisigStatus?.showMultisigStatusError ? 100 : 0 + ), + 2 + )} + + + + {t('TOTAL PENALTIES')} diff --git a/libs/i18n/src/locales/en/governance.json b/libs/i18n/src/locales/en/governance.json index cd85511cc..e540cefaf 100644 --- a/libs/i18n/src/locales/en/governance.json +++ b/libs/i18n/src/locales/en/governance.json @@ -391,6 +391,8 @@ "multisigContractIncorrect": "was incorrectly configured as at the end of the last epoch so rewards were penalised. Validator and delegator rewards will continue to be penalised until this is resolved.", "multisigContractLink": "Ethereum Multisig Contract", "multisigPenalty": "Multisig penalty", + "multisigPenaltyThisNodeIndicator": "The multisig score for this node is equal to zero.", + "multisigPenaltyDescription": "The multisig score is used in the calculation of rewards. For each validator that gets a multisig score of zero, no staking rewards are paid to that consensus validators and their nominators until the epoch following the one in which the configuration issue is resolved.", "myPendingStake": "My pending stake", "myStake": "My stake", "n/a": "N/A", From 2e07ada966d5eca6766a061944377ea21532ec31 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 27 Feb 2024 11:47:10 +0100 Subject: [PATCH 09/21] fix(governance): network paramater change in batch proposals (#5869) --- .../collapsible-toggle/collapsible-toggle.tsx | 11 +++---- .../proposal-header.tsx | 11 ++++--- .../proposal-json/proposal-json.tsx | 2 +- .../proposal-market-changes.tsx | 4 +-- .../proposals/components/proposal/colours.ts | 22 ++++++------- .../components/proposal/indicator.tsx | 9 +++++ .../components/proposal/market-name.tsx | 15 ++++++--- .../proposal/proposal-change-details.tsx | 33 ++++++++++++++++--- .../components/proposal/proposal.tsx | 2 +- .../proposals-list/proposals-list.tsx | 2 +- .../vote-breakdown/vote-breakdown.tsx | 8 ++--- .../src/vega-custom-classes.js | 7 ++-- 12 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 apps/governance/src/routes/proposals/components/proposal/indicator.tsx diff --git a/apps/governance/src/components/collapsible-toggle/collapsible-toggle.tsx b/apps/governance/src/components/collapsible-toggle/collapsible-toggle.tsx index 92b74ae68..250163752 100644 --- a/apps/governance/src/components/collapsible-toggle/collapsible-toggle.tsx +++ b/apps/governance/src/components/collapsible-toggle/collapsible-toggle.tsx @@ -15,19 +15,16 @@ export const CollapsibleToggle = ({ dataTestId, children, }: CollapsibleToggleProps) => { - const classes = classnames( - 'mb-4 transition-transform ease-in-out duration-300', - { - 'rotate-180': toggleState, - } - ); + const classes = classnames('transition-transform ease-in-out duration-300', { + 'rotate-180': toggleState, + }); return (

    diff --git a/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.spec.tsx b/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.spec.tsx index 936fcee8a..73fb7b487 100644 --- a/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.spec.tsx +++ b/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.spec.tsx @@ -1,13 +1,12 @@ import { render, screen } from '@testing-library/react'; import { ProposeNewMarket } from './propose-new-market'; import { MockedProvider } from '@apollo/client/testing'; -import { mockWalletContext } from '../../test-helpers/mocks'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { BrowserRouter as Router } from 'react-router-dom'; import type { MockedResponse } from '@apollo/client/testing'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; +import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing'; jest.mock('@vegaprotocol/environment', () => ({ ...jest.requireActual('@vegaprotocol/environment'), @@ -72,26 +71,27 @@ const newMarketNetworkParamsQueryMock: MockedResponse = { }, }; -const renderComponent = () => - render( +const renderComponent = () => { + return render( - - + + - - + + ); +}; // Note: form submission is tested in propose-raw.spec.tsx. Reusable form // components are tested in their own directory. describe('Propose New Market', () => { - it('should render successfully', async () => { + it('should render successfully', () => { const { baseElement } = renderComponent(); - await expect(baseElement).toBeTruthy(); + expect(baseElement).toBeTruthy(); }); it('should render the form components', async () => { diff --git a/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.tsx b/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.tsx index b637bb5f9..0d28b6971 100644 --- a/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.tsx +++ b/apps/governance/src/routes/proposals/propose/new-market/propose-new-market.tsx @@ -62,7 +62,8 @@ export const ProposeNewMarket = () => { watch, trigger, } = useForm(); - const { finalizedProposal, submit, Dialog } = useProposalSubmit(); + const { finalizedProposal, transaction, submit, setTransaction } = + useProposalSubmit(); const assembleProposal = (fields: NewMarketProposalFormFields) => { const isVoteDeadlineAtMinimum = doesValueEquateToParam( @@ -214,7 +215,8 @@ export const ProposeNewMarket = () => { setTransaction({ dialogOpen: open })} />
    diff --git a/apps/governance/src/routes/proposals/propose/raw/proposal-raw.spec.tsx b/apps/governance/src/routes/proposals/propose/raw/proposal-raw.spec.tsx index 18fcbaa9f..4fc6d9d3e 100644 --- a/apps/governance/src/routes/proposals/propose/raw/proposal-raw.spec.tsx +++ b/apps/governance/src/routes/proposals/propose/raw/proposal-raw.spec.tsx @@ -3,8 +3,6 @@ import type { MockedResponse } from '@apollo/client/testing'; import { addHours, getTime } from 'date-fns'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { MockedProvider } from '@apollo/client/testing'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import * as Schema from '@vegaprotocol/types'; import { ProposeRaw } from './propose-raw'; import { ProposalEventDocument } from '@vegaprotocol/proposals'; @@ -12,6 +10,11 @@ import type { ProposalEventSubscription } from '@vegaprotocol/proposals'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; +import { userRejectedError } from '@vegaprotocol/wallet'; const paramsDelay = 20; @@ -103,23 +106,15 @@ describe('Raw proposal form', () => { }, delay: 300, }; - const setup = (mockSendTx = jest.fn()) => { + const setup = () => { return render( - + - + ); @@ -127,15 +122,22 @@ describe('Raw proposal form', () => { beforeAll(() => { jest.useFakeTimers(); + mockConfig.store.setState({ status: 'connected', pubKey: '0x123' }); }); afterAll(() => { jest.useRealTimers(); + mockConfig.reset(); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('handles validation', async () => { - const mockSendTx = jest.fn().mockReturnValue(Promise.resolve()); - setup(mockSendTx); + const mockSendTx = jest.spyOn(mockConfig, 'sendTransaction'); + + setup(); expect(await screen.findByTestId('proposal-submit')).toBeTruthy(); await act(async () => { @@ -162,20 +164,25 @@ describe('Raw proposal form', () => { }); it('sends the transaction', async () => { - const mockSendTx = jest.fn().mockReturnValue( - new Promise((resolve) => { - setTimeout( - () => - resolve({ - transactionHash: 'tx-hash', - signature: - 'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909', - }), - 100 - ); - }) - ); - setup(mockSendTx); + const mockSendTx = jest + .spyOn(mockConfig, 'sendTransaction') + .mockReturnValue( + new Promise((resolve) => { + setTimeout( + () => + resolve({ + transactionHash: 'tx-hash', + signature: + 'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909', + sentAt: new Date().toISOString(), + receivedAt: new Date().toISOString(), + }), + 100 + ); + }) + ); + + setup(); await act(async () => { jest.advanceTimersByTime(paramsDelay); @@ -206,8 +213,12 @@ describe('Raw proposal form', () => { fireEvent.click(screen.getByTestId('proposal-submit')); }); - expect(mockSendTx).toHaveBeenCalledWith(pubKey, { - proposalSubmission: JSON.parse(inputJSON), + expect(mockSendTx).toHaveBeenCalledWith({ + publicKey: pubKey, + sendingMode: 'TYPE_SYNC', + transaction: { + proposalSubmission: JSON.parse(inputJSON), + }, }); expect(screen.getByTestId('dialog-title')).toHaveTextContent( @@ -232,12 +243,12 @@ describe('Raw proposal form', () => { }); it('can be rejected by the user', async () => { - const mockSendTx = jest.fn().mockReturnValue( - new Promise((resolve) => { - setTimeout(() => resolve(null), 100); + jest.spyOn(mockConfig, 'sendTransaction').mockReturnValue( + new Promise((_, reject) => { + setTimeout(() => reject(userRejectedError()), 100); }) ); - setup(mockSendTx); + setup(); await act(async () => { jest.advanceTimersByTime(paramsDelay); diff --git a/apps/governance/src/routes/proposals/propose/raw/propose-raw.tsx b/apps/governance/src/routes/proposals/propose/raw/propose-raw.tsx index d43a27e92..3b3be2081 100644 --- a/apps/governance/src/routes/proposals/propose/raw/propose-raw.tsx +++ b/apps/governance/src/routes/proposals/propose/raw/propose-raw.tsx @@ -52,7 +52,8 @@ export const ProposeRaw = () => { handleSubmit, formState: { isSubmitting, errors }, } = useForm(); - const { finalizedProposal, submit, Dialog } = useProposalSubmit(); + const { finalizedProposal, transaction, submit, setTransaction } = + useProposalSubmit(); const hasError = Boolean(errors.rawProposalData?.message); @@ -152,7 +153,8 @@ export const ProposeRaw = () => { setTransaction({ dialogOpen: open })} />
    diff --git a/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.spec.tsx b/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.spec.tsx index f4ff11445..ff2525789 100644 --- a/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.spec.tsx +++ b/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.spec.tsx @@ -1,13 +1,12 @@ import { MockedProvider } from '@apollo/client/testing'; import { MemoryRouter as Router } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; -import { mockWalletContext } from '../../test-helpers/mocks'; import { ProposeUpdateAsset } from './propose-update-asset'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import type { MockedResponse } from '@apollo/client/testing'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; +import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing'; jest.mock('@vegaprotocol/environment', () => ({ ...jest.requireActual('@vegaprotocol/environment'), @@ -72,26 +71,27 @@ const updateAssetNetworkParamsQueryMock: MockedResponse = { }, }; -const renderComponent = () => - render( +const renderComponent = () => { + return render( - - + + - - + + ); +}; // Note: form submission is tested in propose-raw.spec.tsx. Reusable form // components are tested in their own directory. describe('Propose Update Asset', () => { - it('should render successfully', async () => { + it('should render successfully', () => { const { baseElement } = renderComponent(); - await expect(baseElement).toBeTruthy(); + expect(baseElement).toBeTruthy(); }); it('should render the title', async () => { diff --git a/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.tsx b/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.tsx index b4aaa30e4..61bc1a918 100644 --- a/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.tsx +++ b/apps/governance/src/routes/proposals/propose/update-asset/propose-update-asset.tsx @@ -62,7 +62,8 @@ export const ProposeUpdateAsset = () => { watch, trigger, } = useForm(); - const { finalizedProposal, submit, Dialog } = useProposalSubmit(); + const { finalizedProposal, transaction, submit, setTransaction } = + useProposalSubmit(); const assembleProposal = (fields: UpdateAssetProposalFormFields) => { const isVoteDeadlineAtMinimum = doesValueEquateToParam( @@ -218,7 +219,8 @@ export const ProposeUpdateAsset = () => { setTransaction({ dialogOpen: open })} />
    diff --git a/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.spec.tsx b/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.spec.tsx index 5b0f097b4..9994f53c7 100644 --- a/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.spec.tsx +++ b/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.spec.tsx @@ -2,15 +2,14 @@ import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import { MemoryRouter as Router } from 'react-router-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; -import { mockWalletContext } from '../../test-helpers/mocks'; import { ProposeUpdateMarket } from './propose-update-market'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import type { ProposalMarketsQueryQuery } from './__generated__/UpdateMarket'; import { ProposalMarketsQueryDocument } from './__generated__/UpdateMarket'; import { ProposalState } from '@vegaprotocol/types'; +import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing'; const updateMarketNetworkParamsQueryMock: MockedResponse = { request: { @@ -217,29 +216,30 @@ const marketQueryMock: MockedResponse = { }, }; -const renderComponent = () => - render( +const renderComponent = () => { + return render( - - + + - - + + ); +}; // Note: form submission is tested in propose-raw.spec.tsx. Reusable form // components are tested in their own directory. describe('Propose Update Market', () => { - it('should render successfully', async () => { + it('should render successfully', () => { const { baseElement } = renderComponent(); - await expect(baseElement).toBeTruthy(); + expect(baseElement).toBeTruthy(); }); it('should render the title', async () => { diff --git a/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.tsx b/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.tsx index cef4c81ac..7df23c1b9 100644 --- a/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.tsx +++ b/apps/governance/src/routes/proposals/propose/update-market/propose-update-market.tsx @@ -109,7 +109,8 @@ export const ProposeUpdateMarket = () => { watch, trigger, } = useForm(); - const { finalizedProposal, submit, Dialog } = useProposalSubmit(); + const { finalizedProposal, transaction, submit, setTransaction } = + useProposalSubmit(); const assembleProposal = (fields: UpdateMarketProposalFormFields) => { const isVoteDeadlineAtMinimum = doesValueEquateToParam( @@ -323,7 +324,8 @@ export const ProposeUpdateMarket = () => { setTransaction({ dialogOpen: open })} />
    diff --git a/apps/governance/src/routes/proposals/test-helpers/mocks.ts b/apps/governance/src/routes/proposals/test-helpers/mocks.ts index 229ae756b..6a12aeaf4 100644 --- a/apps/governance/src/routes/proposals/test-helpers/mocks.ts +++ b/apps/governance/src/routes/proposals/test-helpers/mocks.ts @@ -1,28 +1,17 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters'; import type { MockedResponse } from '@apollo/client/testing'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; -import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VoteValue } from '@vegaprotocol/types'; import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote'; import { UserVoteDocument } from '../components/vote-details/__generated__/Vote'; import faker from 'faker'; +import { type Key } from '@vegaprotocol/wallet'; -export const mockPubkey: PubKey = { +export const mockPubkey: Key = { publicKey: '0x123', name: 'test key 1', }; -export const mockWalletContext = { - pubKey: mockPubkey.publicKey, - pubKeys: [mockPubkey], - isReadOnly: false, - sendTx: jest.fn().mockReturnValue(Promise.resolve(null)), - connect: jest.fn(), - disconnect: jest.fn(), - selectPubKey: jest.fn(), - connector: null, -} as unknown as VegaWalletContextShape; - const mockEthereumConfig = { network_id: '3', chain_id: '3', diff --git a/apps/governance/src/routes/rewards/connect-to-see-rewards.tsx b/apps/governance/src/routes/rewards/connect-to-see-rewards.tsx index c74f6735d..3b1f08e1c 100644 --- a/apps/governance/src/routes/rewards/connect-to-see-rewards.tsx +++ b/apps/governance/src/routes/rewards/connect-to-see-rewards.tsx @@ -1,13 +1,11 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -import { useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useDialogStore } from '@vegaprotocol/wallet-react'; import { Button } from '@vegaprotocol/ui-toolkit'; import { SubHeading } from '../../components/heading'; export const ConnectToSeeRewards = () => { - const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({ - openVegaWalletDialog: store.openVegaWalletDialog, - })); + const openVegaWalletDialog = useDialogStore((store) => store.open); const { t } = useTranslation(); const classes = classNames( diff --git a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx index 06557d3ca..af721f90f 100644 --- a/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx +++ b/apps/governance/src/routes/rewards/epoch-individual-rewards/epoch-individual-rewards.tsx @@ -4,7 +4,7 @@ import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit'; import { removePaginationWrapper } from '@vegaprotocol/utils'; import type { EpochFieldsFragment } from '../home/__generated__/Rewards'; import { useRewardsQuery } from '../home/__generated__/Rewards'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table'; import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list'; import { calculateEpochOffset } from '../../../lib/epoch-pagination'; diff --git a/apps/governance/src/routes/rewards/home/rewards-page.tsx b/apps/governance/src/routes/rewards/home/rewards-page.tsx index 6630d53ca..d88b752b0 100644 --- a/apps/governance/src/routes/rewards/home/rewards-page.tsx +++ b/apps/governance/src/routes/rewards/home/rewards-page.tsx @@ -10,7 +10,7 @@ import { Toggle, ExternalLink, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useNetworkParams, NetworkParams, diff --git a/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.spec.tsx b/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.spec.tsx index 907f964f0..7450bf824 100644 --- a/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.spec.tsx +++ b/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.spec.tsx @@ -33,8 +33,8 @@ let mockVegaWalletHookValue: { pubKey: null, }; -jest.mock('@vegaprotocol/wallet', () => ({ - ...jest.requireActual('@vegaprotocol/wallet'), +jest.mock('@vegaprotocol/wallet-react', () => ({ + ...jest.requireActual('@vegaprotocol/wallet-react'), useVegaWallet: jest.fn(() => mockVegaWalletHookValue), })); diff --git a/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.tsx b/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.tsx index f633471a0..70a94613b 100644 --- a/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.tsx +++ b/apps/governance/src/routes/staking/associate/components/staking-wallets-container/staking-wallets-container.tsx @@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core'; import { useTranslation } from 'react-i18next'; import { EthConnectPrompt } from '../../../../../components/eth-connect-prompt'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { ConnectToVega } from '../../../../../components/connect-to-vega'; export const StakingWalletsContainer = ({ diff --git a/apps/governance/src/routes/staking/disassociate/index.spec.tsx b/apps/governance/src/routes/staking/disassociate/index.spec.tsx index 3df807c42..66dabc944 100644 --- a/apps/governance/src/routes/staking/disassociate/index.spec.tsx +++ b/apps/governance/src/routes/staking/disassociate/index.spec.tsx @@ -33,8 +33,8 @@ let mockVegaWalletHookValue: { pubKey: null, }; -jest.mock('@vegaprotocol/wallet', () => ({ - ...jest.requireActual('@vegaprotocol/wallet'), +jest.mock('@vegaprotocol/wallet-react', () => ({ + ...jest.requireActual('@vegaprotocol/wallet-react'), useVegaWallet: jest.fn(() => mockVegaWalletHookValue), })); diff --git a/apps/governance/src/routes/staking/disassociate/index.tsx b/apps/governance/src/routes/staking/disassociate/index.tsx index 0378e25bc..d062eff51 100644 --- a/apps/governance/src/routes/staking/disassociate/index.tsx +++ b/apps/governance/src/routes/staking/disassociate/index.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useWeb3React } from '@web3-react/core'; import { EthConnectPrompt } from '../../../components/eth-connect-prompt'; import { DisassociatePage } from './components/disassociate-page'; diff --git a/apps/governance/src/routes/staking/home/epoch-data.tsx b/apps/governance/src/routes/staking/home/epoch-data.tsx index 98c011d4d..854c9ed90 100644 --- a/apps/governance/src/routes/staking/home/epoch-data.tsx +++ b/apps/governance/src/routes/staking/home/epoch-data.tsx @@ -5,7 +5,7 @@ import { useStakingQuery } from '../__generated__/Staking'; import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch'; import { ValidatorTables } from './validator-tables'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info'; import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice'; diff --git a/apps/governance/src/routes/staking/node/node.tsx b/apps/governance/src/routes/staking/node/node.tsx index 1fdaeebd0..6ac652ffe 100644 --- a/apps/governance/src/routes/staking/node/node.tsx +++ b/apps/governance/src/routes/staking/node/node.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { addDecimal, removePaginationWrapper, diff --git a/apps/governance/src/routes/staking/node/nodes-container.tsx b/apps/governance/src/routes/staking/node/nodes-container.tsx index 820ef8556..c68c9c15b 100644 --- a/apps/governance/src/routes/staking/node/nodes-container.tsx +++ b/apps/governance/src/routes/staking/node/nodes-container.tsx @@ -1,5 +1,5 @@ import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useTranslation } from 'react-i18next'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { SplashLoader } from '../../../components/splash-loader'; diff --git a/apps/governance/src/routes/staking/node/pending-stake.tsx b/apps/governance/src/routes/staking/node/pending-stake.tsx deleted file mode 100644 index 8007baa01..000000000 --- a/apps/governance/src/routes/staking/node/pending-stake.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import * as Sentry from '@sentry/react'; -import { Button, Callout, Intent, Loader } from '@vegaprotocol/ui-toolkit'; -import { useTranslation } from 'react-i18next'; -import { useAppState } from '../../../contexts/app-state/app-state-context'; -import type { BigNumber } from '../../../lib/bignumber'; -import type { UndelegateSubmissionBody } from '@vegaprotocol/wallet'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { removeDecimal } from '@vegaprotocol/utils'; - -interface PendingStakeProps { - pendingAmount: BigNumber; - nodeId: string; - pubKey: string; -} - -enum FormState { - Default, - Pending, - Success, - Failure, -} - -export const PendingStake = ({ - pendingAmount, - nodeId, - pubKey, -}: PendingStakeProps) => { - const { t } = useTranslation(); - const { sendTx } = useVegaWallet(); - const { appState } = useAppState(); - const [formState, setFormState] = React.useState(FormState.Default); - - const removeStakeNow = async () => { - setFormState(FormState.Pending); - try { - const command: UndelegateSubmissionBody = { - undelegateSubmission: { - nodeId, - amount: removeDecimal(pendingAmount.toString(), appState.decimals), - method: 'METHOD_NOW', - }, - }; - await sendTx(pubKey, command); - } catch (err) { - setFormState(FormState.Failure); - Sentry.captureException(err); - } - }; - - if (formState === FormState.Failure) { - return ( - -

    {t('pleaseTryAgain')}

    -
    - ); - } else if (formState === FormState.Pending) { - return ( - } - title={t('removingPendingStake', { pendingAmount })} - /> - ); - } - - return ( -
    -

    {t('pendingNomination')}

    -

    {t('pendingNominationNextEpoch', { pendingAmount })}

    - -
    - ); -}; diff --git a/apps/governance/src/routes/staking/node/staking-form.tsx b/apps/governance/src/routes/staking/node/staking-form.tsx index a389ba0fc..e1b31b266 100644 --- a/apps/governance/src/routes/staking/node/staking-form.tsx +++ b/apps/governance/src/routes/staking/node/staking-form.tsx @@ -27,11 +27,11 @@ import { NetworkParams, } from '@vegaprotocol/network-parameters'; import { useBalances } from '../../../lib/balances/balances-store'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { SubHeading } from '../../../components/heading'; -import type { - DelegateSubmissionBody, - UndelegateSubmissionBody, +import { + type DelegateSubmissionBody, + type UndelegateSubmissionBody, } from '@vegaprotocol/wallet'; import Routes from '../../routes'; diff --git a/apps/governance/src/routes/withdrawals/index.tsx b/apps/governance/src/routes/withdrawals/index.tsx index 4cbb068f9..eca86dc94 100644 --- a/apps/governance/src/routes/withdrawals/index.tsx +++ b/apps/governance/src/routes/withdrawals/index.tsx @@ -9,7 +9,7 @@ import { useWithdrawalDialog, WithdrawalsTable, } from '@vegaprotocol/withdraws'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDocumentTitle } from '../../hooks/use-document-title'; import type { RouteChildProps } from '../index'; diff --git a/apps/governance/src/toasts-manager.tsx b/apps/governance/src/toasts-manager.tsx index 7ef10a729..b59c90862 100644 --- a/apps/governance/src/toasts-manager.tsx +++ b/apps/governance/src/toasts-manager.tsx @@ -6,7 +6,7 @@ import { VegaIconNames, useToasts, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useDialogStore } from '@vegaprotocol/wallet-react'; import { useEthereumTransactionToasts, useEthereumWithdrawApprovalsToasts, @@ -19,9 +19,7 @@ import { useTranslation } from 'react-i18next'; const WalletDisconnectAdditionalContent = () => { const { t } = useTranslation(); const { hideToast } = useWalletDisconnectToastActions(); - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openVegaWalletDialog = useDialogStore((store) => store.open); return (

    { usePageTitle(t('Create a team')); const { isReadOnly, pubKey } = useVegaWallet(); - const openWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openWalletDialog = useDialogStore((store) => store.open); return ( diff --git a/apps/trading/client-pages/competitions/competitions-team.tsx b/apps/trading/client-pages/competitions/competitions-team.tsx index c5330a724..47d00c594 100644 --- a/apps/trading/client-pages/competitions/competitions-team.tsx +++ b/apps/trading/client-pages/competitions/competitions-team.tsx @@ -31,7 +31,7 @@ import { TeamStats } from '../../components/competitions/team-stats'; import { usePageTitle } from '../../lib/hooks/use-page-title'; import { ErrorBoundary } from '../../components/error-boundary'; import { LayoutWithGradient } from '../../components/layouts-inner'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { JoinTeam } from './join-team'; import { UpdateTeamButton } from './update-team-button'; import { diff --git a/apps/trading/client-pages/competitions/competitions-update-team.tsx b/apps/trading/client-pages/competitions/competitions-update-team.tsx index f8bd9c2d1..d9b859a3c 100644 --- a/apps/trading/client-pages/competitions/competitions-update-team.tsx +++ b/apps/trading/client-pages/competitions/competitions-update-team.tsx @@ -2,7 +2,7 @@ import { ErrorBoundary } from '../../components/error-boundary'; import { usePageTitle } from '../../lib/hooks/use-page-title'; import { Box } from '../../components/competitions/box'; import { useT } from '../../lib/use-t'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { Intent, Loader, @@ -24,9 +24,7 @@ export const CompetitionsUpdateTeam = () => { const t = useT(); usePageTitle([t('Competitions'), t('Update a team')]); const { pubKey, isReadOnly } = useVegaWallet(); - const openWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openWalletDialog = useDialogStore((store) => store.open); const { teamId } = useParams<{ teamId: string }>(); if (!teamId) { return ; diff --git a/apps/trading/client-pages/competitions/join-team.spec.tsx b/apps/trading/client-pages/competitions/join-team.spec.tsx index 5152ef0f3..3cbe1d0cf 100644 --- a/apps/trading/client-pages/competitions/join-team.spec.tsx +++ b/apps/trading/client-pages/competitions/join-team.spec.tsx @@ -29,7 +29,7 @@ describe('JoinButton', () => { }); it('disables button if not connected', async () => { - render(); + render(); const button = screen.getByRole('button'); expect(button).toBeDisabled(); await userEvent.hover(button); diff --git a/apps/trading/client-pages/competitions/join-team.tsx b/apps/trading/client-pages/competitions/join-team.tsx index 47d54db0f..156d52586 100644 --- a/apps/trading/client-pages/competitions/join-team.tsx +++ b/apps/trading/client-pages/competitions/join-team.tsx @@ -6,7 +6,10 @@ import { VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; -import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet'; +import { + useSimpleTransaction, + useVegaWallet, +} from '@vegaprotocol/wallet-react'; import { useT } from '../../lib/use-t'; import { type Team } from '../../lib/hooks/use-team'; import { useState } from 'react'; @@ -59,7 +62,7 @@ export const JoinButton = ({ partyTeam, onJoin, }: { - pubKey: string | null; + pubKey: string | undefined; isReadOnly: boolean; team: Team; partyTeam?: Team; diff --git a/apps/trading/client-pages/competitions/team-form.tsx b/apps/trading/client-pages/competitions/team-form.tsx index 33bc46ef6..af1a38ab1 100644 --- a/apps/trading/client-pages/competitions/team-form.tsx +++ b/apps/trading/client-pages/competitions/team-form.tsx @@ -14,11 +14,11 @@ import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils'; import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction'; import { useT } from '../../lib/use-t'; import { useForm, Controller } from 'react-hook-form'; -import type { - CreateReferralSet, - UpdateReferralSet, - Status, +import { + type CreateReferralSet, + type UpdateReferralSet, } from '@vegaprotocol/wallet'; +import { type Status } from '@vegaprotocol/wallet-react'; import classNames from 'classnames'; import { useLayoutEffect, useState } from 'react'; diff --git a/apps/trading/client-pages/competitions/update-team-button.tsx b/apps/trading/client-pages/competitions/update-team-button.tsx index 0ea312f17..21e08ea0a 100644 --- a/apps/trading/client-pages/competitions/update-team-button.tsx +++ b/apps/trading/client-pages/competitions/update-team-button.tsx @@ -1,6 +1,6 @@ import { type Team } from '../../lib/hooks/use-team'; import { type ComponentProps } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; import { Links } from '../../lib/links'; import { useT } from '../../lib/use-t'; diff --git a/apps/trading/client-pages/deposit/deposit-get-started.tsx b/apps/trading/client-pages/deposit/deposit-get-started.tsx index 832b91551..ccd83d651 100644 --- a/apps/trading/client-pages/deposit/deposit-get-started.tsx +++ b/apps/trading/client-pages/deposit/deposit-get-started.tsx @@ -1,12 +1,12 @@ +import classNames from 'classnames'; import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; import { GetStartedCheckList } from '../../components/welcome-dialog'; +import { useOnboardingStore } from '../../stores/onboarding'; import { useGetOnboardingStep, - useOnboardingStore, OnboardingStep, -} from '../../components/welcome-dialog/use-get-onboarding-step'; +} from '../../lib/hooks/use-get-onboarding-step'; import { Links } from '../../lib/links'; -import classNames from 'classnames'; import { useT } from '../../lib/use-t'; export const DepositGetStarted = () => { diff --git a/apps/trading/client-pages/liquidity/liquidity.tsx b/apps/trading/client-pages/liquidity/liquidity.tsx index cf72e647c..54f8454cf 100644 --- a/apps/trading/client-pages/liquidity/liquidity.tsx +++ b/apps/trading/client-pages/liquidity/liquidity.tsx @@ -1,7 +1,7 @@ import { matchFilter, lpAggregatedDataProvider } from '@vegaprotocol/liquidity'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { Tab, Tabs } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { LiquidityContainer } from '../../components/liquidity-container'; diff --git a/apps/trading/client-pages/markets/closed.spec.tsx b/apps/trading/client-pages/markets/closed.spec.tsx index 1bff48d14..938b201dc 100644 --- a/apps/trading/client-pages/markets/closed.spec.tsx +++ b/apps/trading/client-pages/markets/closed.spec.tsx @@ -1,5 +1,4 @@ import { act, render, screen, waitFor, within } from '@testing-library/react'; -// import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; import { Closed } from './closed'; import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types'; @@ -18,8 +17,6 @@ import { MarketsDocument, getAsset, } from '@vegaprotocol/markets'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { createMarketFragment, @@ -37,7 +34,6 @@ describe('Closed', () => { 3 ).toISOString(); const settlementDateTag = `settlement-expiry-date:${settlementDateMetaDate}`; - const pubKey = 'pubKey'; const marketId = 'market-0'; const settlementDataProperty = 'spec-binding'; const settlementDataId = 'settlement-data-oracle-id'; @@ -175,11 +171,7 @@ describe('Closed', () => { render( - - - + ); diff --git a/apps/trading/client-pages/markets/mobile-buttons.tsx b/apps/trading/client-pages/markets/mobile-buttons.tsx index 4e3118235..8b937a27f 100644 --- a/apps/trading/client-pages/markets/mobile-buttons.tsx +++ b/apps/trading/client-pages/markets/mobile-buttons.tsx @@ -14,7 +14,7 @@ import { useT } from '../../lib/use-t'; import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { useEffect } from 'react'; import classNames from 'classnames'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; const ViewInitializer = () => { const currentRouteId = useGetCurrentRouteId(); @@ -34,9 +34,7 @@ export const MarketsMobileSidebar = () => { const t = useT(); const currentRouteId = useGetCurrentRouteId(); const { pubKeys, isReadOnly } = useVegaWallet(); - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openVegaWalletDialog = useDialogStore((store) => store.open); return ( diff --git a/apps/trading/client-pages/markets/open-markets.spec.tsx b/apps/trading/client-pages/markets/open-markets.spec.tsx index a7c76255e..1f93fa835 100644 --- a/apps/trading/client-pages/markets/open-markets.spec.tsx +++ b/apps/trading/client-pages/markets/open-markets.spec.tsx @@ -15,8 +15,6 @@ import { MarketsDocument, MarketsCandlesDocument, } from '@vegaprotocol/markets'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { marketsQuery, marketsDataQuery, @@ -27,7 +25,6 @@ import userEvent from '@testing-library/user-event'; describe('Open', () => { let originalNow: typeof Date.now; const mockNowTimestamp = 1672531200000; - const pubKey = 'pubKey'; const marketsQueryData = marketsQuery(); const marketsMock: MockedResponse = { @@ -80,11 +77,7 @@ describe('Open', () => { - - - + ); diff --git a/apps/trading/client-pages/referrals/apply-code-form.tsx b/apps/trading/client-pages/referrals/apply-code-form.tsx index a5e1663dd..79865816a 100644 --- a/apps/trading/client-pages/referrals/apply-code-form.tsx +++ b/apps/trading/client-pages/referrals/apply-code-form.tsx @@ -14,8 +14,8 @@ import { RainbowButton } from '../../components/rainbow-button'; import { useSimpleTransaction, useVegaWallet, - useVegaWalletDialogStore, -} from '@vegaprotocol/wallet'; + useDialogStore, +} from '@vegaprotocol/wallet-react'; import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { Routes } from '../../lib/links'; import { Statistics, useStats } from './referral-statistics'; @@ -83,9 +83,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => { const t = useT(); const program = useReferralProgram(); const navigate = useNavigate(); - const openWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openWalletDialog = useDialogStore((store) => store.open); const { isReadOnly, pubKey } = useVegaWallet(); const { isEligible, requiredFunds } = useFundsAvailable(); diff --git a/apps/trading/client-pages/referrals/create-code-form.tsx b/apps/trading/client-pages/referrals/create-code-form.tsx index 12273b3b7..5f4079a1d 100644 --- a/apps/trading/client-pages/referrals/create-code-form.tsx +++ b/apps/trading/client-pages/referrals/create-code-form.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { RainbowButton } from '../../components/rainbow-button'; import { useState } from 'react'; import { @@ -28,9 +28,7 @@ export const CreateCodeContainer = () => { const t = useT(); const { pubKey, isReadOnly } = useVegaWallet(); const isInReferralSet = useIsInReferralSet(pubKey); - const openWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openWalletDialog = useDialogStore((store) => store.open); // Navigate to the index page when already in the referral set. if (isInReferralSet) { diff --git a/apps/trading/client-pages/referrals/hooks/use-funds-available.ts b/apps/trading/client-pages/referrals/hooks/use-funds-available.ts index a80fa6683..21408e696 100644 --- a/apps/trading/client-pages/referrals/hooks/use-funds-available.ts +++ b/apps/trading/client-pages/referrals/hooks/use-funds-available.ts @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useFundsAvailableQuery } from './__generated__/FundsAvailable'; import compact from 'lodash/compact'; import BigNumber from 'bignumber.js'; diff --git a/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx b/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx index 319935af7..d1caf6241 100644 --- a/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx +++ b/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx @@ -6,7 +6,7 @@ import { Button, } from '@vegaprotocol/ui-toolkit'; import { useReferral } from './use-referral'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useEffect } from 'react'; import { useT } from '../../../lib/use-t'; import { matchPath, useLocation, useNavigate } from 'react-router-dom'; diff --git a/apps/trading/client-pages/referrals/hooks/use-referral.ts b/apps/trading/client-pages/referrals/hooks/use-referral.ts index cc8c4bc8b..46050df04 100644 --- a/apps/trading/client-pages/referrals/hooks/use-referral.ts +++ b/apps/trading/client-pages/referrals/hooks/use-referral.ts @@ -15,7 +15,7 @@ export const DEFAULT_AGGREGATION_DAYS = 30; export type Role = 'referrer' | 'referee'; type UseReferralArgs = ( | { code: string | undefined } - | { pubKey: string | null; role: Role } + | { pubKey: string | undefined; role: Role } ) & { aggregationEpochs?: number; }; @@ -188,7 +188,7 @@ const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) => ? data.referralSets.edges[0]?.node : undefined; -export const useIsInReferralSet = (pubKey: string | null) => { +export const useIsInReferralSet = (pubKey: string | undefined) => { const [asRefereeVariables, asRefereeSkip] = prepareVariables({ pubKey, role: 'referee', diff --git a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx index be2f08c33..d680d7c8c 100644 --- a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx +++ b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx @@ -1,9 +1,5 @@ import { MockedProvider, type MockedResponse } from '@apollo/react-testing'; import { render, screen, waitFor } from '@testing-library/react'; -import { - VegaWalletContext, - type VegaWalletContextShape, -} from '@vegaprotocol/wallet'; import { ReferralStatistics } from './referral-statistics'; import { ReferralProgramDocument, @@ -25,9 +21,22 @@ import { type RefereesQuery, } from './hooks/__generated__/Referees'; import { MemoryRouter } from 'react-router-dom'; +import { + mockConfig, + MockedWalletProvider, +} from '@vegaprotocol/wallet-react/testing'; -const MOCK_PUBKEY = - '1234567890123456789012345678901234567890123456789012345678901234'; +const mockKeys = [ + { + name: 'Key 1', + publicKey: '1'.repeat(64), + }, + { + name: 'Key 2', + publicKey: '2'.repeat(64), + }, +]; +const MOCK_PUBKEY = mockKeys[0].publicKey; const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = { networkParameter: { @@ -301,23 +310,29 @@ const refereesMock30: MockedResponse = { describe('ReferralStatistics', () => { const renderComponent = (mocks: MockedResponse[]) => { - const walletContext = { - pubKey: MOCK_PUBKEY, - isReadOnly: false, - sendTx: jest.fn(), - } as unknown as VegaWalletContextShape; - return render( - - + + - - + + ); }; + beforeAll(() => { + mockConfig.store.setState({ + status: 'connected', + keys: mockKeys, + pubKey: mockKeys[0].publicKey, + }); + }); + + afterAll(() => { + mockConfig.reset(); + }); + it('displays apply code when no data has been found for given pubkey', () => { renderComponent([]); expect( diff --git a/apps/trading/client-pages/referrals/referral-statistics.tsx b/apps/trading/client-pages/referrals/referral-statistics.tsx index 75930db5a..2b92eb83e 100644 --- a/apps/trading/client-pages/referrals/referral-statistics.tsx +++ b/apps/trading/client-pages/referrals/referral-statistics.tsx @@ -11,7 +11,7 @@ import { truncateMiddle, TextChildrenTooltip as Tooltip, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { addDecimalsFormatNumber, formatNumber, diff --git a/apps/trading/client-pages/referrals/referrals.tsx b/apps/trading/client-pages/referrals/referrals.tsx index e41df656b..f6f54c117 100644 --- a/apps/trading/client-pages/referrals/referrals.tsx +++ b/apps/trading/client-pages/referrals/referrals.tsx @@ -9,7 +9,7 @@ import { TiersContainer } from './tiers'; import { TabLink } from './buttons'; import { Outlet, useMatch } from 'react-router-dom'; import { Routes } from '../../lib/links'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useReferral } from './hooks/use-referral'; import { REFERRAL_DOCS_LINK } from './constants'; import classNames from 'classnames'; diff --git a/apps/trading/components/accounts-container/accounts-container.tsx b/apps/trading/components/accounts-container/accounts-container.tsx index ba560e694..7b972c040 100644 --- a/apps/trading/components/accounts-container/accounts-container.tsx +++ b/apps/trading/components/accounts-container/accounts-container.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { Splash } from '@vegaprotocol/ui-toolkit'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import type { PinnedAsset } from '@vegaprotocol/accounts'; import { AccountManager } from '@vegaprotocol/accounts'; import { create } from 'zustand'; diff --git a/apps/trading/components/bootstrapper/bootstrapper.tsx b/apps/trading/components/bootstrapper/bootstrapper.tsx index 516ac34a6..422dbbbbd 100644 --- a/apps/trading/components/bootstrapper/bootstrapper.tsx +++ b/apps/trading/components/bootstrapper/bootstrapper.tsx @@ -2,42 +2,24 @@ import type { InMemoryCacheConfig } from '@apollo/client'; import { AppFailure, AppLoader, - DocsLinks, NetworkLoader, NodeFailure, NodeGuard, useEnvironment, } from '@vegaprotocol/environment'; -import { VegaWalletProvider } from '@vegaprotocol/wallet'; -import type { ReactNode } from 'react'; +import { type ReactNode } from 'react'; import { Web3Provider } from './web3-provider'; import { useT } from '../../lib/use-t'; import { DataLoader } from './data-loader'; -import { useChainId } from '@vegaprotocol/wallet'; +import { WalletProvider } from '@vegaprotocol/wallet-react'; +import { useVegaWalletConfig } from '../../lib/hooks/use-vega-wallet-config'; export const Bootstrapper = ({ children }: { children: ReactNode }) => { const t = useT(); - const { - error, - VEGA_URL, - VEGA_ENV, - VEGA_WALLET_URL, - VEGA_EXPLORER_URL, - MOZILLA_EXTENSION_URL, - CHROME_EXTENSION_URL, - } = useEnvironment(); + const { error, VEGA_URL } = useEnvironment(); + const config = useVegaWalletConfig(); - const chainId = useChainId(VEGA_URL); - - if ( - !VEGA_URL || - !VEGA_WALLET_URL || - !VEGA_EXPLORER_URL || - !CHROME_EXTENSION_URL || - !MOZILLA_EXTENSION_URL || - !DocsLinks || - !chainId - ) { + if (!config) { return ; } @@ -72,22 +54,7 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => { } > - - {children} - + {children} diff --git a/apps/trading/components/deposits-container/deposits-container.tsx b/apps/trading/components/deposits-container/deposits-container.tsx index c4c707e73..f0e183de4 100644 --- a/apps/trading/components/deposits-container/deposits-container.tsx +++ b/apps/trading/components/deposits-container/deposits-container.tsx @@ -2,7 +2,7 @@ import { Splash } from '@vegaprotocol/ui-toolkit'; import { DepositsTable } from '@vegaprotocol/deposits'; import { depositsProvider } from '@vegaprotocol/deposits'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useT } from '../../lib/use-t'; export const DepositsContainer = () => { diff --git a/apps/trading/components/fees-container/fees-container.tsx b/apps/trading/components/fees-container/fees-container.tsx index 36104c983..f474e3bc8 100644 --- a/apps/trading/components/fees-container/fees-container.tsx +++ b/apps/trading/components/fees-container/fees-container.tsx @@ -1,6 +1,6 @@ import maxBy from 'lodash/maxBy'; import minBy from 'lodash/minBy'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useNetworkParams, NetworkParams, diff --git a/apps/trading/components/fills-container/fills-container.tsx b/apps/trading/components/fills-container/fills-container.tsx index 3ce056292..61937b04c 100644 --- a/apps/trading/components/fills-container/fills-container.tsx +++ b/apps/trading/components/fills-container/fills-container.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { FillsManager } from '@vegaprotocol/fills'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; diff --git a/apps/trading/components/funding-payments-container/funding-payments-container.tsx b/apps/trading/components/funding-payments-container/funding-payments-container.tsx index 3aa21f936..f909afcff 100644 --- a/apps/trading/components/funding-payments-container/funding-payments-container.tsx +++ b/apps/trading/components/funding-payments-container/funding-payments-container.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { FundingPaymentsManager } from '@vegaprotocol/funding-payments'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; diff --git a/apps/trading/components/ledger-container/ledger-container.tsx b/apps/trading/components/ledger-container/ledger-container.tsx index ba66dd5d3..493ed43db 100644 --- a/apps/trading/components/ledger-container/ledger-container.tsx +++ b/apps/trading/components/ledger-container/ledger-container.tsx @@ -1,6 +1,6 @@ import { LedgerExportForm } from '@vegaprotocol/ledger'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useEnvironment } from '@vegaprotocol/environment'; import type { PartyAssetFieldsFragment } from '@vegaprotocol/assets'; import { usePartyAssetsQuery } from '@vegaprotocol/assets'; diff --git a/apps/trading/components/navbar/navbar.spec.tsx b/apps/trading/components/navbar/navbar.spec.tsx index e413ff4e7..57c1127cb 100644 --- a/apps/trading/components/navbar/navbar.spec.tsx +++ b/apps/trading/components/navbar/navbar.spec.tsx @@ -1,47 +1,38 @@ -import { render, screen, within } from '@testing-library/react'; +import { act, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { Navbar } from './navbar'; import { useGlobalStore } from '../../stores'; import { ENV, useFeatureFlags } from '@vegaprotocol/environment'; +import { + mockConfig, + MockedWalletProvider, +} from '@vegaprotocol/wallet-react/testing'; jest.mock('@vegaprotocol/proposals', () => ({ ProtocolUpgradeCountdown: () => null, })); describe('Navbar', () => { - const pubKey = '000'; - const pubKeys = [ + const mockKeys = [ { - publicKey: pubKey, - name: 'Pub key 0', + name: 'Key 1', + publicKey: '1'.repeat(64), }, { - publicKey: '111', - name: 'Pub key 1', + name: 'Key 2', + publicKey: '2'.repeat(64), }, ]; const marketId = 'abc'; const navbarContent = 'navbar-menu-content'; - const renderComponent = ( - initialEntries?: string[], - walletContext?: Partial - ) => { - const context = { - pubKey, - pubKeys, - selectPubKey: jest.fn(), - disconnect: jest.fn(), - ...walletContext, - } as VegaWalletContextShape; + const renderComponent = (initialEntries?: string[]) => { return render( - + - + ); }; @@ -57,6 +48,12 @@ describe('Navbar', () => { jest.clearAllMocks(); }); + afterEach(() => { + act(() => { + mockConfig.reset(); + }); + }); + it('should be properly rendered', () => { renderComponent(); @@ -125,26 +122,38 @@ describe('Navbar', () => { }); it('can open wallet menu on small screens and change pubkey', async () => { - const mockSelectPubKey = jest.fn(); - renderComponent(undefined, { selectPubKey: mockSelectPubKey }); + mockConfig.store.setState({ + status: 'connected', + keys: mockKeys, + pubKey: mockKeys[0].publicKey, + }); + const mockSelectPubKey = jest.spyOn(mockConfig.store, 'setState'); + renderComponent(undefined); await userEvent.click(screen.getByRole('button', { name: 'Wallet' })); const menuEl = screen.getByTestId(navbarContent); expect(menuEl).toBeInTheDocument(); const menu = within(menuEl); - expect(menu.getAllByTestId(/key-\d+-mobile/)).toHaveLength(pubKeys.length); + expect(menu.getAllByTestId(/key-\d+-mobile/)).toHaveLength(mockKeys.length); - const activeKey = within(menu.getByTestId('key-000-mobile')); - expect(activeKey.getByText(pubKeys[0].name)).toBeInTheDocument(); + const activeKey = within(menu.getByTestId(/key-1+-mobile/)); + expect(activeKey.getByText(mockKeys[0].name)).toBeInTheDocument(); expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument(); - const inactiveKey = within(menu.getByTestId('key-111-mobile')); - await userEvent.click(inactiveKey.getByText(pubKeys[1].name)); - expect(mockSelectPubKey).toHaveBeenCalledWith(pubKeys[1].publicKey); + const inactiveKey = within(menu.getByTestId(/key-2+-mobile/)); + await userEvent.click(inactiveKey.getByText(mockKeys[1].name)); + expect(mockSelectPubKey).toHaveBeenCalledWith({ + pubKey: mockKeys[1].publicKey, + }); }); it('can transfer and close menu', async () => { + mockConfig.store.setState({ + status: 'connected', + keys: mockKeys, + pubKey: mockKeys[0].publicKey, + }); renderComponent(); await userEvent.click(screen.getByRole('button', { name: 'Wallet' })); @@ -158,8 +167,13 @@ describe('Navbar', () => { }); it('can disconnect and close menu', async () => { - const mockDisconnect = jest.fn(); - renderComponent(undefined, { disconnect: mockDisconnect }); + mockConfig.store.setState({ + status: 'connected', + keys: mockKeys, + pubKey: mockKeys[0].publicKey, + }); + const mockDisconnect = jest.spyOn(mockConfig, 'disconnect'); + renderComponent(undefined); await userEvent.click(screen.getByRole('button', { name: 'Wallet' })); const menuEl = screen.getByTestId(navbarContent); diff --git a/apps/trading/components/navbar/navbar.tsx b/apps/trading/components/navbar/navbar.tsx index 476e5465f..4e0909541 100644 --- a/apps/trading/components/navbar/navbar.tsx +++ b/apps/trading/components/navbar/navbar.tsx @@ -25,7 +25,7 @@ import { NavLink } from 'react-router-dom'; import { Links } from '../../lib/links'; import classNames from 'classnames'; import { VegaWalletMenu } from '../vega-wallet'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useDialogStore, useWallet } from '@vegaprotocol/wallet-react'; import { WalletIcon } from '../icons/wallet'; import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals'; import { useT, useI18n } from '../../lib/use-t'; @@ -40,13 +40,9 @@ export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => { // menu state for small screens const [menu, setMenu] = useState(null); - const { pubKey } = useVegaWallet(); + const status = useWallet((store) => store.status); - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); - - const isConnected = pubKey !== null; + const openVegaWalletDialog = useDialogStore((store) => store.open); const navTextClasses = 'text-vega-clight-200 dark:text-vega-cdark-200'; const rootClasses = classNames( @@ -88,7 +84,7 @@ export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {

    { - if (isConnected) { + if (status === 'connected') { setMenu((x) => (x === 'wallet' ? null : 'wallet')); } else { openVegaWalletDialog(); diff --git a/apps/trading/components/orders-container/orders-container.tsx b/apps/trading/components/orders-container/orders-container.tsx index c61af42e0..03520f805 100644 --- a/apps/trading/components/orders-container/orders-container.tsx +++ b/apps/trading/components/orders-container/orders-container.tsx @@ -1,7 +1,7 @@ import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { Filter, OrderListManager } from '@vegaprotocol/orders'; import { Splash } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; diff --git a/apps/trading/components/positions-container/positions-container.tsx b/apps/trading/components/positions-container/positions-container.tsx index cb303c162..476fd1dc4 100644 --- a/apps/trading/components/positions-container/positions-container.tsx +++ b/apps/trading/components/positions-container/positions-container.tsx @@ -1,7 +1,7 @@ import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { PositionsManager } from '@vegaprotocol/positions'; import { Splash } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import type { StateCreator } from 'zustand'; diff --git a/apps/trading/components/rewards-container/rewards-container.spec.tsx b/apps/trading/components/rewards-container/rewards-container.spec.tsx index a96bd5253..09199f9e0 100644 --- a/apps/trading/components/rewards-container/rewards-container.spec.tsx +++ b/apps/trading/components/rewards-container/rewards-container.spec.tsx @@ -172,7 +172,9 @@ describe('Vesting', () => { }); it('doesnt use multiplier if not connected', () => { - render(); + render( + + ); expect(screen.getByTestId('vesting-rate')).toHaveTextContent('25%'); @@ -199,7 +201,11 @@ describe('Multipliers', () => { it('shows not connected state', () => { render( - + ); expect( diff --git a/apps/trading/components/rewards-container/rewards-container.tsx b/apps/trading/components/rewards-container/rewards-container.tsx index 887d360e5..4686ff403 100644 --- a/apps/trading/components/rewards-container/rewards-container.tsx +++ b/apps/trading/components/rewards-container/rewards-container.tsx @@ -7,7 +7,7 @@ import { useNetworkParams, } from '@vegaprotocol/network-parameters'; import { AccountType } from '@vegaprotocol/types'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import BigNumber from 'bignumber.js'; import { Card, @@ -309,7 +309,7 @@ type VestingBalances = NonNullable< >['vestingBalancesSummary']; export type RewardPotProps = { - pubKey: string | null; + pubKey: string | undefined; accounts: Account[] | null; assetId: string; // VEGA vestingBalancesSummary: VestingBalances | undefined; @@ -469,7 +469,7 @@ export const Vesting = ({ baseRate, multiplier, }: { - pubKey: string | null; + pubKey: string | undefined; baseRate: string; multiplier?: string; }) => { @@ -506,7 +506,7 @@ export const Multipliers = ({ streakMultiplier, hoarderMultiplier, }: { - pubKey: string | null; + pubKey: string | undefined; streakMultiplier?: string; hoarderMultiplier?: string; }) => { diff --git a/apps/trading/components/rewards-container/rewards-history.tsx b/apps/trading/components/rewards-container/rewards-history.tsx index f56284a63..f71940cde 100644 --- a/apps/trading/components/rewards-container/rewards-history.tsx +++ b/apps/trading/components/rewards-container/rewards-history.tsx @@ -25,7 +25,7 @@ export const RewardsHistoryContainer = ({ pubKey, assets, }: { - pubKey: string | null; + pubKey: string | undefined; epoch: number; assets: Record; }) => { @@ -129,7 +129,7 @@ export const RewardHistoryTable = ({ epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries']; partyRewards: PartyRewardsConnection; assets: Record | null; - pubKey: string | null; + pubKey: string | undefined; epoch: number; epochVariables: { from: number; @@ -145,7 +145,7 @@ export const RewardHistoryTable = ({ epochRewardSummaries, partyRewards, assets, - partyId: isParty ? pubKey : null, + partyId: isParty ? pubKey : undefined, }); const columnDefs = useMemo[]>(() => { diff --git a/apps/trading/components/rewards-container/use-reward-row-data.ts b/apps/trading/components/rewards-container/use-reward-row-data.ts index 5aa6bbd58..43da4535e 100644 --- a/apps/trading/components/rewards-container/use-reward-row-data.ts +++ b/apps/trading/components/rewards-container/use-reward-row-data.ts @@ -97,7 +97,7 @@ export const useRewardsRowData = ({ partyRewards: PartyRewardsConnection; epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries']; assets: Record | null; - partyId: string | null; + partyId: string | undefined; }) => { if (partyId) { const rewards = removePaginationWrapper(partyRewards?.edges).map((r) => ({ diff --git a/apps/trading/components/risk-ack-content/index.ts b/apps/trading/components/risk-ack-content/index.ts new file mode 100644 index 000000000..cbfd7de9d --- /dev/null +++ b/apps/trading/components/risk-ack-content/index.ts @@ -0,0 +1 @@ +export { RiskAckContent } from './risk-ack-content'; diff --git a/apps/trading/components/welcome-dialog/risk-message.tsx b/apps/trading/components/risk-ack-content/risk-ack-content.tsx similarity index 76% rename from apps/trading/components/welcome-dialog/risk-message.tsx rename to apps/trading/components/risk-ack-content/risk-ack-content.tsx index a2cf2860a..a1d2a0a96 100644 --- a/apps/trading/components/welcome-dialog/risk-message.tsx +++ b/apps/trading/components/risk-ack-content/risk-ack-content.tsx @@ -1,23 +1,14 @@ -import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; -import { Link } from 'react-router-dom'; -import { Links } from '../../lib/links'; -import { useT } from '../../lib/use-t'; import { Trans } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import { useT } from '../../lib/use-t'; +import { Links } from '../../lib/links'; -const DisclaimerLink = ({ children }: { children?: string[] }) => ( - - - {children} - - - -); - -export const RiskMessage = () => { +export const RiskAckContent = () => { const t = useT(); return ( <> -
    +
    • {t( @@ -34,7 +25,7 @@ export const RiskMessage = () => {
    -

    +

    ]} @@ -43,3 +34,14 @@ export const RiskMessage = () => { ); }; + +const DisclaimerLink = ({ children }: { children?: string[] }) => ( + + {children} + + +); diff --git a/apps/trading/components/sidebar/sidebar.spec.tsx b/apps/trading/components/sidebar/sidebar.spec.tsx index 28be0e085..41cc8ce8c 100644 --- a/apps/trading/components/sidebar/sidebar.spec.tsx +++ b/apps/trading/components/sidebar/sidebar.spec.tsx @@ -9,9 +9,8 @@ import { } from './sidebar'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { VegaIconNames } from '@vegaprotocol/ui-toolkit'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { Routes as AppRoutes } from '../../lib/links'; +import { MockedWalletProvider } from '@vegaprotocol/wallet-react/testing'; jest.mock('../node-health', () => ({ NodeHealthContainer: () => , @@ -33,10 +32,6 @@ jest.mock('../welcome-dialog', () => ({ GetStarted: () =>

    , })); -const walletContext = { - pubKeys: [{ publicKey: 'pubkey' }], -} as VegaWalletContextShape; - describe('Sidebar', () => { beforeEach(() => { useSidebar.setState({ views: {} }); @@ -44,14 +39,13 @@ describe('Sidebar', () => { it('renders options prop', () => { render( - - + + } /> - - + + ); - expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId('node-health')).toBeInTheDocument(); expect(screen.getByTestId('options')).toBeInTheDocument(); @@ -60,8 +54,8 @@ describe('Sidebar', () => { it('renders selected state', async () => { const routeId = '/markets/ABC'; render( - - + + { /> } /> - - + + ); const settingsButton = screen.getByTestId(ViewType.Settings); @@ -102,8 +96,8 @@ describe('SidebarContent', () => { it('renders the correct content', () => { const { container } = render( - - + + { element={} /> - - + + ); expect(container).toBeEmptyDOMElement(); @@ -136,8 +130,8 @@ describe('SidebarContent', () => { it('closes sidebar if market id is required but not present', () => { const { container } = render( - - + + { element={} /> - - + + ); act(() => { @@ -176,7 +170,7 @@ describe('SidebarContent', () => { }); describe('SidebarButton', () => { - it.each([ViewType.Info, ViewType.Deposit, ViewType.ViewAs])( + it.each([ViewType.Info, ViewType.Deposit])( 'runs given callback regardless of requested view (%s)', async (view) => { const onClick = jest.fn(); diff --git a/apps/trading/components/sidebar/sidebar.tsx b/apps/trading/components/sidebar/sidebar.tsx index 628e574d6..bcb06c8cd 100644 --- a/apps/trading/components/sidebar/sidebar.tsx +++ b/apps/trading/components/sidebar/sidebar.tsx @@ -13,7 +13,7 @@ import { Settings } from '../settings'; import { Tooltip } from '../../components/tooltip'; import { WithdrawContainer } from '../withdraw-container'; import { GetStarted } from '../welcome-dialog'; -import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet'; +import { useConnect, usePubKeys } from '@vegaprotocol/wallet-react'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useT } from '../../lib/use-t'; import { ErrorBoundary } from '../error-boundary'; @@ -57,11 +57,11 @@ export type BarView = }; export const Sidebar = ({ options }: { options?: ReactNode }) => { + const { connect } = useConnect(); + const { pubKeys } = usePubKeys(); const t = useT(); const currentRouteId = useGetCurrentRouteId(); const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; - const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); - const { pubKeys } = useVegaWallet(); const { isMobile } = useScreenDimensions(); const { getView } = useSidebar((store) => ({ setViews: store.setViews, @@ -83,12 +83,15 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => { <> { - setViewAsDialogOpen(true); + onClick={async () => { + const res = await connect('viewParty'); + if (res.status !== 'connected') { + window.alert('Failed to connect'); + } }} icon={VegaIconNames.EYE} - tooltip={t('View as party')} - disabled={Boolean(pubKeys)} + tooltip={t('View as public key')} + disabled={Boolean(pubKeys.length)} routeId={currentRouteId} /> ({ - ...jest.requireActual('@vegaprotocol/wallet'), - useVegaWalletDialogStore: () => mockUpdateDialogOpen, -})); +import { + mockConfig, + MockedWalletProvider, +} from '@vegaprotocol/wallet-react/testing'; jest.mock('../../lib/hooks/use-get-current-route-id', () => ({ useGetCurrentRouteId: jest.fn().mockReturnValue('current-route-id'), })); -beforeEach(() => { - jest.clearAllMocks(); -}); - -const generateJsx = (context: VegaWalletContextShape) => { +const renderComponent = (mockOnClick = jest.fn()) => { return ( - - - + + + ); }; describe('VegaWalletConnectButton', () => { - it('should fire dialog when not connected', () => { - render(generateJsx({ pubKey: null } as VegaWalletContextShape)); + afterEach(() => { + act(() => { + mockConfig.reset(); + }); + }); + it('should fire dialog when not connected', async () => { + const onClick = jest.fn(); + render(renderComponent(onClick)); const button = screen.getByTestId('connect-vega-wallet'); expect(button).toHaveTextContent('Get started'); - fireEvent.click(button); - expect(mockUpdateDialogOpen).toHaveBeenCalled(); + await userEvent.click(button); + expect(onClick).toHaveBeenCalled(); }); - it('should render "Connect" when browser wallet is detected', () => { + it('should render "Connect" when browser wallet is detected', async () => { window.vega = window.vega || ({} as Vega); - render(generateJsx({ pubKey: null } as VegaWalletContextShape)); - + render(renderComponent()); const button = screen.getByTestId('connect-vega-wallet'); expect(button).toHaveTextContent('Connect'); + }); + + 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, + pubKey: key.publicKey, + }); + + const refreshKeys = jest.spyOn(mockConfig, 'refreshKeys'); + const disconnect = jest.spyOn(mockConfig, 'disconnect'); + const setPubKey = jest.spyOn(mockConfig.store, 'setState'); + + render(renderComponent()); + + expect(screen.queryByTestId('connect-vega-wallet')).not.toBeInTheDocument(); + const button = screen.getByTestId('manage-vega-wallet'); + expect(button).toHaveTextContent(truncateByChars(key.publicKey)); + fireEvent.click(button); - expect(mockUpdateDialogOpen).toHaveBeenCalled(); - }); - it('should retrieve keys when connected', async () => { - const pubKey = { publicKey: '123456__123456', name: 'test' }; - const pubKey2 = { publicKey: '123456__123457', name: 'test2' }; - render( - generateJsx({ - pubKey: pubKey.publicKey, - pubKeys: [pubKey], - fetchPubKeys: () => Promise.resolve([pubKey, pubKey2]), - } as VegaWalletContextShape) + expect(await screen.findByRole('menu')).toBeInTheDocument(); + expect(await screen.findAllByRole('menuitemradio')).toHaveLength( + keys.length ); + expect(refreshKeys).toHaveBeenCalled(); - const button = screen.getByTestId('manage-vega-wallet'); - expect(button).toHaveTextContent(truncateByChars(pubKey.publicKey)); - userEvent.click(button); - expect(mockUpdateDialogOpen).not.toHaveBeenCalled(); - }); + fireEvent.click(screen.getByTestId(`key-${key2.publicKey}`)); + expect(setPubKey).toHaveBeenCalledWith({ pubKey: key2.publicKey }); - it('should fetch keys when connected', async () => { - const pubKey = { publicKey: '123456__123456', name: 'test' }; - const context = { - pubKey: pubKey.publicKey, - pubKeys: [pubKey], - } as VegaWalletContextShape; - render(generateJsx(context)); - - const button = screen.getByTestId('manage-vega-wallet'); - expect(button).toHaveTextContent(truncateByChars(pubKey.publicKey)); - userEvent.click(button); - expect(mockUpdateDialogOpen).not.toHaveBeenCalled(); + fireEvent.click(screen.getByTestId('disconnect')); + expect(disconnect).toHaveBeenCalled(); }); }); diff --git a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx index dd26d0708..1f610a784 100644 --- a/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx +++ b/apps/trading/components/vega-wallet-connect-button/vega-wallet-connect-button.tsx @@ -1,6 +1,5 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { isBrowserWalletInstalled } from '@vegaprotocol/wallet'; import { truncateByChars } from '@vegaprotocol/utils'; import { VegaIcon, @@ -16,11 +15,11 @@ import { TradingDropdownRadioItem, TradingDropdownItemIndicator, } from '@vegaprotocol/ui-toolkit'; -import type { PubKey } from '@vegaprotocol/wallet'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { isBrowserWalletInstalled, type Key } from '@vegaprotocol/wallet'; +import { useDialogStore, useVegaWallet } from '@vegaprotocol/wallet-react'; import { useCopyTimeout } from '@vegaprotocol/react-helpers'; -import { ViewType, useSidebar } from '../sidebar'; 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'; @@ -33,26 +32,24 @@ export const VegaWalletConnectButton = ({ }) => { const t = useT(); const [dropdownOpen, setDropdownOpen] = useState(false); - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openVegaWalletDialog = useDialogStore((store) => store.open); const currentRouteId = useGetCurrentRouteId(); const setViews = useSidebar((store) => store.setViews); const { - pubKey, + status, pubKeys, + pubKey, selectPubKey, disconnect, + refreshKeys, isReadOnly, - fetchPubKeys, } = useVegaWallet(); - const isConnected = pubKey !== null; - const walletInstalled = isBrowserWalletInstalled(); - const activeKey = useMemo(() => { - return pubKeys?.find((pk) => pk.publicKey === pubKey); - }, [pubKey, pubKeys]); - if (isConnected && pubKeys) { + const walletInstalled = isBrowserWalletInstalled(); + + const activeKey = pubKeys?.find((pk) => pk.publicKey === pubKey); + + if (status === 'connected') { return ( { - if (fetchPubKeys) { - fetchPubKeys(); - } - setDropdownOpen(!dropdownOpen); + refreshKeys(); + setDropdownOpen((x) => !x); }} > } @@ -86,7 +89,7 @@ export const VegaWalletConnectButton = ({ >
    { selectPubKey(value); }} @@ -138,7 +141,7 @@ export const VegaWalletConnectButton = ({ ); }; -const KeypairItem = ({ pk, active }: { pk: PubKey; active: boolean }) => { +const KeypairItem = ({ pk, active }: { pk: Key; active: boolean }) => { const t = useT(); const [copied, setCopied] = useCopyTimeout(); diff --git a/apps/trading/components/vega-wallet-connect-dialog/index.ts b/apps/trading/components/vega-wallet-connect-dialog/index.ts new file mode 100644 index 000000000..f2b43955a --- /dev/null +++ b/apps/trading/components/vega-wallet-connect-dialog/index.ts @@ -0,0 +1 @@ +export { VegaWalletConnectDialog } from './vega-wallet-connect-dialog'; diff --git a/apps/trading/components/vega-wallet-connect-dialog/vega-wallet-connect-dialog.tsx b/apps/trading/components/vega-wallet-connect-dialog/vega-wallet-connect-dialog.tsx new file mode 100644 index 000000000..174f2f0f8 --- /dev/null +++ b/apps/trading/components/vega-wallet-connect-dialog/vega-wallet-connect-dialog.tsx @@ -0,0 +1,30 @@ +import { + ConnectDialogWithRiskAck, + useDialogStore, +} from '@vegaprotocol/wallet-react'; +import { useOnboardingStore } from '../../stores/onboarding'; +import { Networks, useEnvironment } from '@vegaprotocol/environment'; +import { RiskAckContent } from '../risk-ack-content'; + +export const VegaWalletConnectDialog = () => { + const { VEGA_ENV } = useEnvironment(); + const open = useDialogStore((store) => store.isOpen); + const setDialog = useDialogStore((store) => store.set); + const risk = useOnboardingStore((store) => store.risk); + const acceptRisk = useOnboardingStore((store) => store.acceptRisk); + const rejectRisk = useOnboardingStore((store) => store.rejectRisk); + + return ( + { + rejectRisk(); + setDialog(false); + }} + riskAckContent={} + /> + ); +}; diff --git a/apps/trading/components/vega-wallet/vega-wallet-menu.tsx b/apps/trading/components/vega-wallet/vega-wallet-menu.tsx index 0adbdf0bc..02a341980 100644 --- a/apps/trading/components/vega-wallet/vega-wallet-menu.tsx +++ b/apps/trading/components/vega-wallet/vega-wallet-menu.tsx @@ -5,7 +5,8 @@ import { VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import { truncateByChars } from '@vegaprotocol/utils'; -import { useVegaWallet, type PubKey } from '@vegaprotocol/wallet'; +import { type Key } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useCallback, useMemo } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { ViewType, useSidebar } from '../sidebar'; @@ -73,7 +74,7 @@ const KeypairListItem = ({ isActive, onSelectItem, }: { - pk: PubKey; + pk: Key; isActive: boolean; onSelectItem: (pk: string) => void; }) => { diff --git a/apps/trading/components/viewing-banner/index.tsx b/apps/trading/components/viewing-banner/index.tsx index 1d6adc884..880242b7b 100644 --- a/apps/trading/components/viewing-banner/index.tsx +++ b/apps/trading/components/viewing-banner/index.tsx @@ -1,5 +1,5 @@ import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; export const ViewingBanner = () => { const { isReadOnly, pubKey, disconnect } = useVegaWallet(); diff --git a/apps/trading/components/welcome-dialog/get-started.spec.tsx b/apps/trading/components/welcome-dialog/get-started.spec.tsx index aef0f5688..7f7a10abe 100644 --- a/apps/trading/components/welcome-dialog/get-started.spec.tsx +++ b/apps/trading/components/welcome-dialog/get-started.spec.tsx @@ -1,23 +1,26 @@ import { MemoryRouter } from 'react-router-dom'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; import { GetStarted } from './get-started'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { useOnboardingStore } from './use-get-onboarding-step'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { useOnboardingStore } from '../../stores/onboarding'; let mockStep = 1; -jest.mock('./use-get-onboarding-step', () => ({ - ...jest.requireActual('./use-get-onboarding-step'), + +jest.mock('../../lib/hooks/use-get-onboarding-step', () => ({ + ...jest.requireActual('../../lib/hooks/use-get-onboarding-step'), useGetOnboardingStep: jest.fn(() => mockStep), })); describe('GetStarted', () => { - const renderComponent = (context: Partial = {}) => { - return render( + const renderComponent = () => { + return ( - + - + ); }; @@ -33,26 +36,34 @@ describe('GetStarted', () => { jest.clearAllMocks(); }); + afterEach(() => { + act(() => { + mockConfig.reset(); + }); + }); + it('renders full get started content if not connected and no browser wallet detected', () => { - renderComponent(); + render(renderComponent()); expect(screen.getByTestId('get-started-banner')).toBeInTheDocument(); }); it('renders connect prompt if no pubKey but wallet installed', () => { globalThis.window.vega = {} as Vega; - renderComponent(); + render(renderComponent()); expect(screen.getByTestId('get-started-banner')).toBeInTheDocument(); globalThis.window.vega = undefined as unknown as Vega; }); it('renders nothing if dismissed', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); useOnboardingStore.setState({ dismissed: true }); mockStep = 0; - const { container } = renderComponent({ pubKey: 'my-pubkey' }); + const { container } = render(renderComponent()); expect(container).toBeEmptyDOMElement(); }); it('steps should be ticked', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); useOnboardingStore.setState({ dismissed: false }); const navigatorGetter: jest.SpyInstance = jest.spyOn( window.navigator, @@ -62,29 +73,17 @@ describe('GetStarted', () => { navigatorGetter.mockReturnValue('Chrome'); mockStep = 2; - const { rerender, container } = renderComponent(); + const { rerender, container } = render(renderComponent()); checkTicks(screen.getAllByRole('listitem')); expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument(); mockStep = 3; - rerender( - - - - - - ); + rerender(renderComponent()); checkTicks(screen.getAllByRole('listitem')); expect(screen.getByRole('link', { name: 'Deposit' })).toBeInTheDocument(); mockStep = 4; - rerender( - - - - - - ); + rerender(renderComponent()); checkTicks(screen.getAllByRole('listitem')); expect( screen.getByRole('link', { name: 'Ready to trade' }) @@ -93,15 +92,7 @@ describe('GetStarted', () => { fireEvent.click(screen.getByRole('link', { name: 'Ready to trade' })); mockStep = 5; - rerender( - - - - - - ); + rerender(renderComponent()); expect(container).toBeEmptyDOMElement(); }); }); diff --git a/apps/trading/components/welcome-dialog/get-started.tsx b/apps/trading/components/welcome-dialog/get-started.tsx index fd0c64e84..b693c59d8 100644 --- a/apps/trading/components/welcome-dialog/get-started.tsx +++ b/apps/trading/components/welcome-dialog/get-started.tsx @@ -7,13 +7,13 @@ import { VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; +import { useVegaWallet, useDialogStore } from '@vegaprotocol/wallet-react'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; +import { useOnboardingStore } from '../../stores/onboarding'; import { OnboardingStep, useGetOnboardingStep, - useOnboardingStore, -} from './use-get-onboarding-step'; +} from '../../lib/hooks/use-get-onboarding-step'; import { Links, Routes } from '../../lib/links'; import { useGlobalStore } from '../../stores'; import { useSidebar, ViewType } from '../sidebar'; @@ -26,12 +26,11 @@ interface Props { const GetStartedButton = ({ step }: { step: OnboardingStep }) => { const t = useT(); + const { VEGA_ENV } = useEnvironment(); const dismiss = useOnboardingStore((store) => store.dismiss); - const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen); + const setDialog = useOnboardingStore((store) => store.setDialog); + const risk = useOnboardingStore((store) => store.risk); const marketId = useGlobalStore((store) => store.marketId); - const setWalletDialogOpen = useOnboardingStore( - (store) => store.setWalletDialogOpen - ); const setViews = useSidebar((store) => store.setViews); const buttonProps = { @@ -42,7 +41,16 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { if (step <= OnboardingStep.ONBOARDING_CONNECT_STEP) { return ( - setWalletDialogOpen(true)}> + { + if (VEGA_ENV === Networks.MAINNET && risk !== 'accepted') { + setDialog('risk'); + } else { + setDialog('connect'); + } + }} + > {t('Connect')} ); @@ -51,7 +59,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { setDialogOpen(false)} + onClick={() => setDialog('inactive')} > {t('Deposit')} @@ -73,7 +81,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { } return ( - setWalletDialogOpen(true)}> + setDialog('connect')}> {t('Get started')} ); @@ -110,9 +118,7 @@ export const GetStarted = ({ lead }: Props) => { const t = useT(); const { pubKey } = useVegaWallet(); const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment(); - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); + const openVegaWalletDialog = useDialogStore((store) => store.open); const currentStep = useGetOnboardingStep(); const dismissed = useOnboardingStore((store) => store.dismissed); @@ -151,7 +157,11 @@ export const GetStarted = ({ lead }: Props) => { + Switch to Mainnet , ]} diff --git a/apps/trading/components/welcome-dialog/index.ts b/apps/trading/components/welcome-dialog/index.ts index 2698067c1..f5e62f884 100644 --- a/apps/trading/components/welcome-dialog/index.ts +++ b/apps/trading/components/welcome-dialog/index.ts @@ -1,3 +1,2 @@ export * from './welcome-dialog'; -export * from './risk-message'; export * from './get-started'; diff --git a/apps/trading/components/welcome-dialog/use-get-onboarding-step.spec.tsx b/apps/trading/components/welcome-dialog/use-get-onboarding-step.spec.tsx deleted file mode 100644 index d53272614..000000000 --- a/apps/trading/components/welcome-dialog/use-get-onboarding-step.spec.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { ReactNode } from 'react'; -import { renderHook } from '@testing-library/react'; -import { - useGetOnboardingStep, - OnboardingStep, -} from './use-get-onboarding-step'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import { ordersWithMarketProvider } from '@vegaprotocol/orders'; -import { positionsDataProvider } from '@vegaprotocol/positions'; - -let mockData: object[] | null = [{ id: 'item-id' }]; -jest.mock('@vegaprotocol/data-provider', () => ({ - ...jest.requireActual('@vegaprotocol/data-provider'), - useDataProvider: jest.fn(() => ({ data: mockData })), -})); - -let mockContext: Partial = { pubKey: 'test-pubkey' }; - -describe('useGetOnboardingStep', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockData = []; - mockContext = { pubKey: 'test-pubkey' }; - globalThis.window.vega = {} as Vega; - }); - - const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - - ); - - it('should return properly ONBOARDING_UNKNOWN_STEP', () => { - mockData = null; - const { result } = renderHook(() => useGetOnboardingStep(), { wrapper }); - expect(result.current).toEqual(OnboardingStep.ONBOARDING_UNKNOWN_STEP); - }); - - it('should return properly ONBOARDING_CONNECT_STEP', () => { - mockContext = { pubKey: null }; - const { result } = renderHook(() => useGetOnboardingStep(), { wrapper }); - expect(result.current).toEqual(OnboardingStep.ONBOARDING_CONNECT_STEP); - }); - - it('should return properly ONBOARDING_DEPOSIT_STEP', async () => { - const { result } = renderHook(() => useGetOnboardingStep(), { wrapper }); - await expect(result.current).toEqual( - OnboardingStep.ONBOARDING_DEPOSIT_STEP - ); - }); - - it('should return properly ONBOARDING_ORDER_STEP', async () => { - mockData = [{ id: 'item-id' }]; - (useDataProvider as jest.Mock).mockImplementation((args) => { - if ( - args.dataProvider === ordersWithMarketProvider || - args.dataProvider === positionsDataProvider - ) { - return { data: [] }; - } - return { data: mockData }; - }); - const { result } = renderHook(() => useGetOnboardingStep(), { wrapper }); - await expect(result.current).toEqual(OnboardingStep.ONBOARDING_ORDER_STEP); - }); - - it('should return properly ONBOARDING_COMPLETE_STEP', async () => { - mockData = [{ id: 'item-id' }]; - (useDataProvider as jest.Mock).mockImplementation(() => { - return { data: mockData }; - }); - const { result } = renderHook(() => useGetOnboardingStep(), { wrapper }); - await expect(result.current).toEqual( - OnboardingStep.ONBOARDING_COMPLETE_STEP - ); - }); -}); diff --git a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx index 353fb0a30..b939015b8 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx @@ -4,15 +4,14 @@ import { Links } from '../../lib/links'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; import type { ReactNode } from 'react'; import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets'; -import { useOnboardingStore } from './use-get-onboarding-step'; +import { useOnboardingStore } from '../../stores/onboarding'; import { useT } from '../../lib/use-t'; export const WelcomeDialogContent = () => { const t = useT(); const { VEGA_ENV } = useEnvironment(); - const setOnboardingDialog = useOnboardingStore( - (store) => store.setDialogOpen - ); + const setDialog = useOnboardingStore((store) => store.setDialog); + const dismiss = useOnboardingStore((store) => store.dismiss); const { data } = useTopTradedMarkets(); const marketId = data && data[0]?.id; @@ -25,40 +24,46 @@ export const WelcomeDialogContent = () => { 'Free from the risks of real trading, Fairground is a safe and fun place to try out Vega yourself with virtual assets.' ); return ( -
    -
    -
      - } - title={t('Non-custodial and pseudonymous')} - text={t('No third party has access to your funds.')} - /> - } - title={t('Purpose built proof of stake blockchain')} - text={t( - 'Fully decentralised high performance peer-to-network trading.' - )} - /> - } - title={t('Low fees and no cost to place orders')} - text={t( - 'Fees work like a CEX with no per-transaction gas for orders' - )} - /> -
    - setOnboardingDialog(false)} - className="block w-full" - data-testid="browse-markets-button" - > - {t('Explore')} - -
    -
    - +
    +

    {t('Console {{env}}', { env: VEGA_ENV })}

    +
    +
    +
      + } + title={t('Non-custodial and pseudonymous')} + text={t('No third party has access to your funds.')} + /> + } + title={t('Purpose built proof of stake blockchain')} + text={t( + 'Fully decentralised high performance peer-to-network trading.' + )} + /> + } + title={t('Low fees and no cost to place orders')} + text={t( + 'Fees work like a CEX with no per-transaction gas for orders' + )} + /> +
    + { + setDialog('inactive'); + dismiss(); + }} + className="block w-full" + data-testid="browse-markets-button" + > + {t('Explore')} + +
    +
    + +
    ); @@ -74,7 +79,7 @@ const ListItemContent = ({ text: string; }) => { return ( -
  • +
  • {icon}

    {title}

    diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx index e63feaec8..191c1304d 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -1,15 +1,25 @@ import { useEffect } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; import { Dialog, Intent } from '@vegaprotocol/ui-toolkit'; -import { useEnvironment } from '@vegaprotocol/environment'; -import { VegaConnectDialog } from '@vegaprotocol/wallet'; -import { useConnectors } from '../../lib/vega-connectors'; +import { + ConnectionStatus, + useConnect, + useWallet, + Links, + RiskAck, + ConnectionOptionRecord, + ConnectionOptionDefault, +} from '@vegaprotocol/wallet-react'; +import { ConnectorErrors } from '@vegaprotocol/wallet'; +import { ensureSuffix } from '@vegaprotocol/utils'; import { useT } from '../../lib/use-t'; import { Routes } from '../../lib/links'; -import { RiskMessage } from './risk-message'; import { WelcomeDialogContent } from './welcome-dialog-content'; -import { useOnboardingStore } from './use-get-onboarding-step'; -import { ensureSuffix } from '@vegaprotocol/utils'; +import { + useOnboardingStore, + type OnboardingDialog, +} from '../../stores/onboarding'; +import { RiskAckContent } from '../risk-ack-content'; /** * A list of paths on which the welcome dialog should be omitted. @@ -18,19 +28,10 @@ const OMIT_ON_LIST = [ensureSuffix(Routes.REFERRALS, '/*')]; export const WelcomeDialog = () => { const { pathname } = useLocation(); - const t = useT(); - const { VEGA_ENV } = useEnvironment(); - const connectors = useConnectors(); const dismissed = useOnboardingStore((store) => store.dismissed); - const dialogOpen = useOnboardingStore((store) => store.dialogOpen); + const dialog = useOnboardingStore((store) => store.dialog); const dismiss = useOnboardingStore((store) => store.dismiss); - const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen); - const walletDialogOpen = useOnboardingStore( - (store) => store.walletDialogOpen - ); - const setWalletDialogOpen = useOnboardingStore( - (store) => store.setWalletDialogOpen - ); + const setDialog = useOnboardingStore((store) => store.setDialog); useEffect(() => { const shouldOmit = OMIT_ON_LIST.map((path) => @@ -39,46 +40,132 @@ export const WelcomeDialog = () => { if (dismissed || shouldOmit) return; - setDialogOpen(true); - }, [dismissed, pathname, setDialogOpen]); + setDialog('intro'); + }, [dismissed, pathname, setDialog]); - const content = walletDialogOpen ? ( - } - onClose={() => setWalletDialogOpen(false)} - contentOnly - /> - ) : ( - - ); - - const onClose = walletDialogOpen - ? () => setWalletDialogOpen(false) - : () => { - setDialogOpen(false); - dismiss(); - }; - - const title = walletDialogOpen ? null : ( - - {t('Console')}{' '} - - {VEGA_ENV} - - - ); + const onClose = () => { + if (dialog === 'connect' || dialog === 'risk') { + setDialog('intro'); + } else { + setDialog('inactive'); + dismiss(); + } + }; return ( - {content} + { + setTimeout(() => setDialog('intro'), 1000); + }} + /> ); }; + +const DialogStepSwitch = ({ + dialog, + onConnect, +}: { + dialog: OnboardingDialog; + onConnect: () => void; +}) => { + const accept = useOnboardingStore((store) => store.acceptRisk); + const reject = useOnboardingStore((store) => store.rejectRisk); + const setDialog = useOnboardingStore((store) => store.setDialog); + + if (dialog === 'intro') { + return ; + } + + if (dialog === 'connect') { + return ; + } + + if (dialog === 'risk') { + return ( + { + accept(); + setDialog('connect'); + }} + onReject={() => { + reject(); + setDialog('intro'); + }} + > + + + ); + } +}; + +const OnboardConnectionOptions = ({ onConnect }: { onConnect: () => void }) => { + const t = useT(); + const error = useWallet((store) => store.error); + const { connect, connectors } = useConnect(); + const status = useWallet((store) => store.status); + + if (status === 'disconnected') { + return ( +
    +

    {t('Connect to Vega')}

    +
    +
      + {connectors.map((c) => { + const ConnectionOption = ConnectionOptionRecord[c.id]; + const props = { + id: c.id, + name: c.name, + description: c.description, + showDescription: true, + onClick: async () => { + const res = await connect(c.id); + if (res.status === 'connected') { + onConnect(); + } + }, + }; + + if (ConnectionOption) { + return ( +
    • + +
    • + ); + } + + return ( +
    • + +
    • + ); + })} +
    + {error && error.code !== ConnectorErrors.userRejected.code && ( +

    + {error.message} +

    + )} + + Dont have a wallet? + +
    +
    + ); + } + + return ; +}; diff --git a/apps/trading/components/withdraw-container/withdraw-container.tsx b/apps/trading/components/withdraw-container/withdraw-container.tsx index faa76f6ac..28b2f4faa 100644 --- a/apps/trading/components/withdraw-container/withdraw-container.tsx +++ b/apps/trading/components/withdraw-container/withdraw-container.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { WithdrawFormContainer } from '@vegaprotocol/withdraws'; import { useVegaTransactionStore } from '@vegaprotocol/web3'; diff --git a/apps/trading/components/withdrawals-container/withdrawals-container.tsx b/apps/trading/components/withdrawals-container/withdrawals-container.tsx index 434b757df..3acf453ed 100644 --- a/apps/trading/components/withdrawals-container/withdrawals-container.tsx +++ b/apps/trading/components/withdrawals-container/withdrawals-container.tsx @@ -4,7 +4,7 @@ import { WithdrawalsTable, useIncompleteWithdrawals, } from '@vegaprotocol/withdraws'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { useT } from '../../lib/use-t'; diff --git a/apps/trading/e2e/conftest.py b/apps/trading/e2e/conftest.py index 1574db6e9..520d5d484 100644 --- a/apps/trading/e2e/conftest.py +++ b/apps/trading/e2e/conftest.py @@ -112,6 +112,7 @@ def init_vega(request=None): extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")}, ) vega.container = container + logger.info(f"Container ID: {container.id}, Name: {container.name}, Status: {container.status}") yield vega except APIError as e: logger.info(f"Container creation failed.") @@ -120,8 +121,10 @@ def init_vega(request=None): finally: logger.info(f"Stopping container {container.id}") container.stop() + logger.info(f"Container ID: {container.id}, Name: {container.name}, Status: {container.status}") logger.info(f"Removing container {container.id}") container.remove() + logger.info(f"Container ID: {container.id}, Name: {container.name}, Status: {container.status}") @contextmanager @@ -182,29 +185,33 @@ def vega(request): request.addfinalizer(lambda: cleanup_container(vega_instance)) yield vega_instance - - def cleanup_container(vega_instance): - try: - # Attempt to stop the container if it's still running - if vega_instance.container.status == 'running': - print(f"Stopping container {vega_instance.container.id}") - vega_instance.container.stop() - else: - print(f"Container {vega_instance.container.id} is not running.") - except docker.errors.NotFound: - print(f"Container {vega_instance.container.id} not found, may have been stopped and removed.") - except Exception as e: - print(f"Error during cleanup: {str(e)}") + # Ensure there is a list of containers to work with + if hasattr(vega_instance, 'containers') and vega_instance.containers: + for container in vega_instance.containers: + try: + # Check if each container is still running and then stop it + if container.status == 'running': + print(f"Stopping container {container.id}") + container.stop() + else: + print(f"Container {container.id} is not running.") + except docker.errors.NotFound: + print(f"Container {container.id} not found, may have been stopped and removed.") + except Exception as e: + print(f"Error during cleanup for container {container.id}: {str(e)}") + + try: + # Attempt to remove the container after stopping it + container.remove() + print(f"Container {container.id} removed.") + except docker.errors.NotFound: + print(f"Container {container.id} not found, may have been removed.") + except Exception as e: + print(f"Error during container removal for container {container.id}: {str(e)}") + else: + print("No containers to cleanup.") - try: - # Attempt to remove the container - vega_instance.container.remove() - print(f"Container {vega_instance.container.id} removed.") - except docker.errors.NotFound: - print(f"Container {vega_instance.container.id} not found, may have been removed.") - except Exception as e: - print(f"Error during container removal: {str(e)}") @pytest.fixture def page(vega, browser, request): @@ -221,22 +228,36 @@ def auth_setup(vega: VegaServiceNull, page: Page): wallet_api_token = vega.wallet.login_tokens[DEFAULT_WALLET_NAME] # Set token to localStorage so eager connect hook picks it up and immediately connects + # no pubkey is set so it should default to the first pubkey wallet_config = json.dumps( { - "token": f"VWT {wallet_api_token}", - "connector": "jsonRpc", - "url": f"http://localhost:{vega.wallet_port}", + "state":{ + "chainId":"CUSTOM", + "current":"jsonRpc", + "jsonRpcToken": f"VWT {wallet_api_token}", + }, + "version":0 } ) + onboarding_config = json.dumps({ + "state": { + "dismissed": True, + "risk": "accepted" + }, + "version": 0 + }) - storage_javascript = [ - # Store wallet config so eager connection is initiated - f"localStorage.setItem('vega_wallet_config', '{wallet_config}');", - # Ensure wallet ris dialog doesnt show, otherwise eager connect wont work - "localStorage.setItem('vega_wallet_risk_accepted', 'true');", - # Ensure initial risk dialog doesnt show - "localStorage.setItem('vega_risk_accepted', 'true');", - ] + # Before we set the wallet store, get any currently stored key and set it as well. + # This way if you navigate the page, reload or start a new test, the last used key + # will be persisted. Playwright will clear storage on every test and navigation event + storage_javascript = f""" + var currentStorage = JSON.parse(localStorage.getItem('vega_wallet_store')); + var defaultStorage = JSON.parse('{wallet_config}'); + var pubKey = currentStorage?.state?.pubKey; + defaultStorage['state']['pubKey'] = pubKey || undefined; + localStorage.setItem('vega_wallet_store', JSON.stringify(defaultStorage)); + localStorage.setItem('vega_onboarding', '{onboarding_config}'); + """ script = "".join(storage_javascript) page.add_init_script(script) @@ -254,9 +275,14 @@ def auth(vega: VegaServiceNull, page: Page): # Set 'risk accepted' flag, so that the risk dialog doesn't show up def risk_accepted_setup(page: Page): - onboarding_config = json.dumps({"state": {"dismissed": True}, "version": 0}) + onboarding_config = json.dumps({ + "state": { + "dismissed": True, + "risk": "accepted" + }, + "version": 0 + }) storage_javascript = [ - "localStorage.setItem('vega_risk_accepted', 'true');", f"localStorage.setItem('vega_onboarding', '{onboarding_config}');", "localStorage.setItem('vega_telemetry_approval', 'false');", "localStorage.setItem('vega_telemetry_viewed', 'true');", @@ -303,14 +329,14 @@ def perps_market(vega, request): @pytest.fixture(autouse=True) def retry_on_http_error(request): - retry_count = 3 + retry_count = 3 for i in range(retry_count): try: yield - return + return except requests.exceptions.HTTPError: if i < retry_count - 1: print(f"Retrying due to HTTPError (attempt {i+1}/{retry_count})") else: - raise + raise diff --git a/apps/trading/e2e/tests/get_started/test_get_started.py b/apps/trading/e2e/tests/get_started/test_get_started.py index 34438813a..2231694df 100644 --- a/apps/trading/e2e/tests/get_started/test_get_started.py +++ b/apps/trading/e2e/tests/get_started/test_get_started.py @@ -43,16 +43,19 @@ class TestGetStarted: # Set token to localStorage so eager connect hook picks it up and immediately connects wallet_config = json.dumps( - { - "token": f"VWT {wallet_api_token}", - "connector": "jsonRpc", - "url": f"http://localhost:{vega.wallet_port}", - } - ) + { + "state":{ + "chainId":"CUSTOM", + "current":"jsonRpc", + "jsonRpcToken": f"VWT {wallet_api_token}", + }, + "version":0 + } + ) storage_javascript = [ # Store wallet config so eager connection is initiated - f"localStorage.setItem('vega_wallet_config', '{wallet_config}');", + f"localStorage.setItem('vega_wallet_store', '{wallet_config}');", # Ensure wallet ris dialog doesnt show, otherwise eager connect wont work "localStorage.setItem('vega_wallet_risk_accepted', 'true');", # Ensure initial risk dialog doesnt show diff --git a/apps/trading/e2e/tests/navigation/test_navigation.py b/apps/trading/e2e/tests/navigation/test_navigation.py index 461d74a3e..6d2d595ac 100644 --- a/apps/trading/e2e/tests/navigation/test_navigation.py +++ b/apps/trading/e2e/tests/navigation/test_navigation.py @@ -52,8 +52,8 @@ def test_navigation_mobile(page: Page): wallet_button = navbar.get_by_test_id("navbar-mobile-wallet") expect(wallet_button).to_be_visible() wallet_button.click() - dialog = page.get_by_test_id("dialog-content") - expect(dialog.get_by_test_id("wallet-dialog-title")).to_be_visible() + + expect(page.get_by_test_id("dialog-content").nth(1)).to_be_visible() # endregion diff --git a/apps/trading/lib/hooks/use-get-onboarding-step.spec.tsx b/apps/trading/lib/hooks/use-get-onboarding-step.spec.tsx new file mode 100644 index 000000000..cc8943bf3 --- /dev/null +++ b/apps/trading/lib/hooks/use-get-onboarding-step.spec.tsx @@ -0,0 +1,82 @@ +import { act, renderHook } from '@testing-library/react'; +import { + useGetOnboardingStep, + OnboardingStep, +} from './use-get-onboarding-step'; +import { useDataProvider } from '@vegaprotocol/data-provider'; +import { ordersWithMarketProvider } from '@vegaprotocol/orders'; +import { positionsDataProvider } from '@vegaprotocol/positions'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; + +let mockData: object[] | null = [{ id: 'item-id' }]; +jest.mock('@vegaprotocol/data-provider', () => ({ + ...jest.requireActual('@vegaprotocol/data-provider'), + useDataProvider: jest.fn(() => ({ data: mockData })), +})); + +describe('useGetOnboardingStep', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockData = []; + globalThis.window.vega = {} as Vega; + }); + + afterEach(() => { + act(() => { + mockConfig.reset(); + }); + }); + + const setup = () => { + return renderHook(() => useGetOnboardingStep(), { + wrapper: MockedWalletProvider, + }); + }; + + it('should return properly ONBOARDING_UNKNOWN_STEP', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); + mockData = null; + const { result } = setup(); + expect(result.current).toEqual(OnboardingStep.ONBOARDING_UNKNOWN_STEP); + }); + + it('should return properly ONBOARDING_CONNECT_STEP', () => { + const { result } = setup(); + expect(result.current).toEqual(OnboardingStep.ONBOARDING_CONNECT_STEP); + }); + + it('should return properly ONBOARDING_DEPOSIT_STEP', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); + const { result } = setup(); + expect(result.current).toEqual(OnboardingStep.ONBOARDING_DEPOSIT_STEP); + }); + + it('should return properly ONBOARDING_ORDER_STEP', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); + mockData = [{ id: 'item-id' }]; + (useDataProvider as jest.Mock).mockImplementation((args) => { + if ( + args.dataProvider === ordersWithMarketProvider || + args.dataProvider === positionsDataProvider + ) { + return { data: [] }; + } + return { data: mockData }; + }); + const { result } = setup(); + expect(result.current).toEqual(OnboardingStep.ONBOARDING_ORDER_STEP); + }); + + it('should return properly ONBOARDING_COMPLETE_STEP', () => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); + mockData = [{ id: 'item-id' }]; + (useDataProvider as jest.Mock).mockImplementation(() => { + return { data: mockData }; + }); + const { result } = setup(); + expect(result.current).toEqual(OnboardingStep.ONBOARDING_COMPLETE_STEP); + }); +}); diff --git a/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts b/apps/trading/lib/hooks/use-get-onboarding-step.ts similarity index 64% rename from apps/trading/components/welcome-dialog/use-get-onboarding-step.ts rename to apps/trading/lib/hooks/use-get-onboarding-step.ts index 753ca9e3c..763816368 100644 --- a/apps/trading/components/welcome-dialog/use-get-onboarding-step.ts +++ b/apps/trading/lib/hooks/use-get-onboarding-step.ts @@ -1,41 +1,10 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { depositsProvider } from '@vegaprotocol/deposits'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { ordersWithMarketProvider } from '@vegaprotocol/orders'; import * as Types from '@vegaprotocol/types'; import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts'; import { positionsDataProvider } from '@vegaprotocol/positions'; -import { useGlobalStore } from '../../stores'; - -const ONBOARDING_STORAGE_KEY = 'vega_onboarding'; - -export const useOnboardingStore = create<{ - dialogOpen: boolean; - walletDialogOpen: boolean; - dismissed: boolean; - dismiss: () => void; - setDialogOpen: (isOpen: boolean) => void; - setWalletDialogOpen: (isOpen: boolean) => void; -}>()( - persist( - (set) => ({ - dialogOpen: false, - walletDialogOpen: false, - dismissed: false, - dismiss: () => set({ dismissed: true }), - setDialogOpen: (isOpen) => set({ dialogOpen: isOpen }), - setWalletDialogOpen: (isOpen) => set({ walletDialogOpen: isOpen }), - }), - { - name: ONBOARDING_STORAGE_KEY, - partialize: (state) => ({ - dismissed: state.dismissed, - }), - } - ) -); export enum OnboardingStep { ONBOARDING_UNKNOWN_STEP, @@ -47,8 +16,7 @@ export enum OnboardingStep { } export const useGetOnboardingStep = () => { - const connecting = useGlobalStore((store) => store.eagerConnecting); - const { pubKey = '', pubKeys } = useVegaWallet(); + const { status, pubKey = '', pubKeys } = useVegaWallet(); const { data: depositsData } = useDataProvider({ dataProvider: depositsProvider, variables: { partyId: pubKey || '' }, @@ -87,11 +55,12 @@ export const useGetOnboardingStep = () => { const positions = Boolean(positionsData?.length); const isLoading = Boolean( - (connecting || pubKey) && - (depositsData === null || - ordersData === null || - collateralData === null || - positionsData === null) + status === 'connecting' || + (pubKey && + (depositsData === null || + ordersData === null || + collateralData === null || + positionsData === null)) ); let step = OnboardingStep.ONBOARDING_UNKNOWN_STEP; if (isLoading) { diff --git a/apps/trading/lib/hooks/use-my-team.ts b/apps/trading/lib/hooks/use-my-team.ts index d394d9052..00ffd6e60 100644 --- a/apps/trading/lib/hooks/use-my-team.ts +++ b/apps/trading/lib/hooks/use-my-team.ts @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import compact from 'lodash/compact'; import first from 'lodash/first'; import { useTeamsQuery } from './__generated__/Teams'; diff --git a/apps/trading/lib/hooks/use-referral-set-transaction.ts b/apps/trading/lib/hooks/use-referral-set-transaction.ts index 9d65267cd..727718423 100644 --- a/apps/trading/lib/hooks/use-referral-set-transaction.ts +++ b/apps/trading/lib/hooks/use-referral-set-transaction.ts @@ -1,9 +1,8 @@ import { - useSimpleTransaction, - type Options, type CreateReferralSet, type UpdateReferralSet, } from '@vegaprotocol/wallet'; +import { useSimpleTransaction, type Options } from '@vegaprotocol/wallet-react'; import { useStakeAvailable } from './use-stake-available'; /** diff --git a/apps/trading/lib/hooks/use-stake-available.ts b/apps/trading/lib/hooks/use-stake-available.ts index 6c500697a..d89581997 100644 --- a/apps/trading/lib/hooks/use-stake-available.ts +++ b/apps/trading/lib/hooks/use-stake-available.ts @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useStakeAvailableQuery } from './__generated__/StakeAvailable'; /** diff --git a/apps/trading/lib/hooks/use-vega-wallet-config.ts b/apps/trading/lib/hooks/use-vega-wallet-config.ts new file mode 100644 index 000000000..2fb28663d --- /dev/null +++ b/apps/trading/lib/hooks/use-vega-wallet-config.ts @@ -0,0 +1,46 @@ +import { + InjectedConnector, + JsonRpcConnector, + SnapConnector, + ViewPartyConnector, + createConfig, + fairground, + mainnet, + stagnet, +} from '@vegaprotocol/wallet'; +import { CHAIN_IDS, useEnvironment } from '@vegaprotocol/environment'; +import { useMemo } from 'react'; + +/** + * Hook for memoizing the vega wallet config, since we can't be certain of the + * node address for snap connection we need to re-new once its set or changes + */ +export const useVegaWalletConfig = () => { + const { VEGA_ENV, VEGA_URL, VEGA_WALLET_URL } = useEnvironment(); + + return useMemo(() => { + if (!VEGA_URL || !VEGA_WALLET_URL) return; + + const injected = new InjectedConnector(); + + const jsonRpc = new JsonRpcConnector({ + url: VEGA_WALLET_URL, + }); + + const snap = new SnapConnector({ + node: new URL(VEGA_URL).origin, + snapId: 'npm:@vegaprotocol/snap', + version: '1.0.1', + }); + + const viewParty = new ViewPartyConnector(); + + const config = createConfig({ + chains: [mainnet, fairground, stagnet], + defaultChainId: CHAIN_IDS[VEGA_ENV], + connectors: [injected, snap, jsonRpc, viewParty], + }); + + return config; + }, [VEGA_ENV, VEGA_URL, VEGA_WALLET_URL]); +}; diff --git a/apps/trading/lib/i18n/index.ts b/apps/trading/lib/i18n/index.ts index c49602393..b53ceae2d 100644 --- a/apps/trading/lib/i18n/index.ts +++ b/apps/trading/lib/i18n/index.ts @@ -56,7 +56,8 @@ i18n fallbackLng: 'en', supportedLngs, load: 'languageOnly', - // have a common namespace used around the full app + // load all these namespaces immediately on start up. If the name space is not here, + // it will only be loaded once used by useTranslation ns: [ 'accounts', 'assets', @@ -79,6 +80,7 @@ i18n 'ui-toolkit', 'utils', 'wallet', + 'wallet-react', 'web3', ], defaultNS: 'trading', diff --git a/apps/trading/lib/vega-connectors.ts b/apps/trading/lib/vega-connectors.ts deleted file mode 100644 index 2da3166dd..000000000 --- a/apps/trading/lib/vega-connectors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useFeatureFlags } from '@vegaprotocol/environment'; -import { useMemo } from 'react'; -import { - JsonRpcConnector, - ViewConnector, - InjectedConnector, - SnapConnector, - DEFAULT_SNAP_ID, -} from '@vegaprotocol/wallet'; - -export const jsonRpc = new JsonRpcConnector(); -export const injected = new InjectedConnector(); - -let view: ViewConnector; -if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.hash.split('?')[1]); - view = new ViewConnector(urlParams.get('address')); -} else { - view = new ViewConnector(); -} - -export const snap = new SnapConnector(DEFAULT_SNAP_ID); - -export const useConnectors = () => { - const featureFlags = useFeatureFlags((state) => state.flags); - return useMemo( - () => ({ - injected, - jsonRpc, - view, - snap: featureFlags.METAMASK_SNAPS ? snap : undefined, - }), - [featureFlags.METAMASK_SNAPS] - ); -}; diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx index 851709672..d59499164 100644 --- a/apps/trading/pages/dialogs-container.tsx +++ b/apps/trading/pages/dialogs-container.tsx @@ -2,25 +2,19 @@ import { AssetDetailsDialog, useAssetDetailsDialogStore, } from '@vegaprotocol/assets'; -import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet'; -import { useConnectors } from '../lib/vega-connectors'; import { Web3ConnectUncontrolledDialog, WithdrawalApprovalDialogContainer, } from '@vegaprotocol/web3'; import { WelcomeDialog } from '../components/welcome-dialog'; -import { RiskMessage } from '../components/welcome-dialog'; +import { VegaWalletConnectDialog } from '../components/vega-wallet-connect-dialog'; const DialogsContainer = () => { const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore(); - const connectors = useConnectors(); + return ( <> - } - /> - + { const { VEGA_ENV, SENTRY_DSN } = useEnvironment(); - const update = useGlobalStore((store) => store.update); - const connectors = useConnectors(); - const eagerConnecting = useVegaEagerConnect(connectors); const [isTelemetryApproved] = useTelemetryApproval(); useEthereumEagerConnect( isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {} ); + useVegaEagerConnect(); - const { pubKey, connect } = useVegaWallet(); - const [searchParams] = useSearchParams(); - const [query] = useState(searchParams.get('address')); - if (query && !pubKey) { - connect(connectors.view); - } - useEffect(() => { - update({ eagerConnecting }); - }, [update, eagerConnecting]); return null; }; diff --git a/apps/trading/pages/party-active-orders-handler.tsx b/apps/trading/pages/party-active-orders-handler.tsx index 7878f780a..8e60ead80 100644 --- a/apps/trading/pages/party-active-orders-handler.tsx +++ b/apps/trading/pages/party-active-orders-handler.tsx @@ -1,6 +1,6 @@ import { useDataProvider } from '@vegaprotocol/data-provider'; import { activeOrdersProvider } from '@vegaprotocol/orders'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; export const PartyActiveOrdersHandler = () => { const { pubKey } = useVegaWallet(); diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index e83687eae..e5b69fd04 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -4,7 +4,6 @@ import produce from 'immer'; interface GlobalStore { marketId: string | null; - eagerConnecting: boolean; update: (store: Partial>) => void; } @@ -15,7 +14,6 @@ interface PageTitleStore { export const useGlobalStore = create()((set) => ({ marketId: LocalStorage.getItem('marketId') || null, - eagerConnecting: false, update: (newState) => { set( produce((state: GlobalStore) => { diff --git a/apps/trading/stores/onboarding.ts b/apps/trading/stores/onboarding.ts new file mode 100644 index 000000000..d75435277 --- /dev/null +++ b/apps/trading/stores/onboarding.ts @@ -0,0 +1,40 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +const ONBOARDING_STORAGE_KEY = 'vega_onboarding'; + +export type RiskStatus = 'pending' | 'accepted' | 'rejected'; +export type OnboardingDialog = 'inactive' | 'intro' | 'risk' | 'connect'; + +export const useOnboardingStore = create<{ + dialog: OnboardingDialog; + dismissed: boolean; + dismiss: () => void; + setDialog: (step: OnboardingDialog) => void; + risk: RiskStatus; + acceptRisk: () => void; + rejectRisk: () => void; +}>()( + persist( + (set) => ({ + dialog: 'inactive', + dismissed: false, + dismiss: () => set({ dismissed: true }), + setDialog: (step) => set({ dialog: step }), + risk: 'pending', + acceptRisk: () => { + set({ risk: 'accepted' }); + }, + rejectRisk: () => { + set({ risk: 'rejected' }); + }, + }), + { + name: ONBOARDING_STORAGE_KEY, + partialize: (state) => ({ + dismissed: state.dismissed, + risk: state.risk, + }), + } + ) +); diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index 4d57e17f9..4d268a4bc 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -217,7 +217,7 @@ export const aggregatedAccountDataProvider = makeDerivedDataProvider< ) || null ); -export const useAccounts = (partyId: string | null) => { +export const useAccounts = (partyId: string | undefined) => { return useDataProvider({ dataProvider: accountsDataProvider, variables: { diff --git a/libs/accounts/src/lib/transfer-container.tsx b/libs/accounts/src/lib/transfer-container.tsx index 84ab0d1d1..a294651fa 100644 --- a/libs/accounts/src/lib/transfer-container.tsx +++ b/libs/accounts/src/lib/transfer-container.tsx @@ -10,7 +10,7 @@ import { import { useDataProvider } from '@vegaprotocol/data-provider'; import type { Transfer } from '@vegaprotocol/wallet'; import { useVegaTransactionStore } from '@vegaprotocol/web3'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useCallback } from 'react'; import { accountsDataProvider } from './accounts-data-provider'; import { TransferForm } from './transfer-form'; diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx index a44697273..90b61fa68 100644 --- a/libs/accounts/src/lib/transfer-form.tsx +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -44,7 +44,7 @@ interface Asset { } export interface TransferFormProps { - pubKey: string | null; + pubKey: string | undefined; pubKeys: string[] | null; isReadOnly?: boolean; accounts: Array<{ diff --git a/libs/accounts/src/lib/use-account-balance.tsx b/libs/accounts/src/lib/use-account-balance.tsx index adbfc5a99..9d93a5e44 100644 --- a/libs/accounts/src/lib/use-account-balance.tsx +++ b/libs/accounts/src/lib/use-account-balance.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { accountsDataProvider } from './accounts-data-provider'; import type { Account } from './accounts-data-provider'; diff --git a/libs/accounts/src/lib/use-margin-account-balance.tsx b/libs/accounts/src/lib/use-margin-account-balance.tsx index 87403b2a0..37dc22148 100644 --- a/libs/accounts/src/lib/use-margin-account-balance.tsx +++ b/libs/accounts/src/lib/use-margin-account-balance.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { accountsDataProvider } from './accounts-data-provider'; import type { Account } from './accounts-data-provider'; diff --git a/libs/candles-chart/src/lib/candles-chart.tsx b/libs/candles-chart/src/lib/candles-chart.tsx index 9f3a8bd4d..1ee7fd3ba 100644 --- a/libs/candles-chart/src/lib/candles-chart.tsx +++ b/libs/candles-chart/src/lib/candles-chart.tsx @@ -11,7 +11,7 @@ import { useApolloClient } from '@apollo/client'; import { useMemo } from 'react'; import debounce from 'lodash/debounce'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useT } from './use-t'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers'; diff --git a/libs/cypress/src/lib/commands/add-connect-public-key.ts b/libs/cypress/src/lib/commands/add-connect-public-key.ts index 02efc1d81..45efb749f 100644 --- a/libs/cypress/src/lib/commands/add-connect-public-key.ts +++ b/libs/cypress/src/lib/commands/add-connect-public-key.ts @@ -10,12 +10,14 @@ declare global { export const addConnectPublicKey = () => { Cypress.Commands.add('connectPublicKey', (publicKey) => { + // Intercept the prompt + cy.window().then((win) => { + cy.stub(win, 'prompt').returns(publicKey); + }); + const connectVegaWaletBtn = Cypress.$(`[data-testid="view-as-user"]`); if (connectVegaWaletBtn.length > 0) { cy.get('aside button').contains('View as party').click(); - cy.getByTestId('address').should('be.visible').focus(); - cy.getByTestId('address').type(publicKey, { delay: 50 }); - cy.getByTestId('connect').click(); } }); }; diff --git a/libs/cypress/src/lib/commands/vega-wallet-connect.ts b/libs/cypress/src/lib/commands/vega-wallet-connect.ts index 64d430a8f..d3e6e9486 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-connect.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-connect.ts @@ -47,6 +47,7 @@ export function addVegaWalletConnect() { return; } cy.wrap(btn).click(); + cy.wait(200); cy.get('[data-testid=connectors-list]') .find('[data-testid="connector-jsonRpc"]') .click(); @@ -61,7 +62,10 @@ export function addVegaWalletConnect() { }); } -const onboardingViewedState = { state: { dismissed: true }, version: 0 }; +const onboardingViewedState = { + state: { dismissed: true, risk: 'accepted' }, + version: 0, +}; export function addSetVegaWallet() { Cypress.Commands.add('setVegaWallet', () => { @@ -73,11 +77,13 @@ export function addSetVegaWallet() { win.localStorage.setItem('vega_telemetry_approval', 'false'); win.localStorage.setItem('vega_telemetry_viewed', 'true'); win.localStorage.setItem( - 'vega_wallet_config', + 'vega_wallet_store', JSON.stringify({ - token: `VWT ${Cypress.env('VEGA_WALLET_API_TOKEN')}`, - connector: 'jsonRpc', - url: 'http://localhost:1789', + state: { + jsonRpcToken: `VWT ${Cypress.env('VEGA_WALLET_API_TOKEN')}`, + current: 'jsonRpc', + }, + version: 0, }) ); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx index d2fa7a014..8a34b2c0e 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-margin-details.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; import { getAsset, getQuoteName } from '@vegaprotocol/markets'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; import { formatRange, formatValue } from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx index 6558dfa34..8659d40e2 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { generateMarket } from '../../test-helpers'; @@ -13,6 +12,10 @@ import { } from '../../hooks/use-form-values'; import { useFeatureFlags } from '@vegaprotocol/environment'; import { formatForInput } from '@vegaprotocol/utils'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; jest.mock('zustand'); jest.mock('./deal-ticket-fee-details', () => ({ @@ -23,12 +26,12 @@ const marketPrice = '200'; const market = generateMarket(); const submit = jest.fn(); -function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) { +function generateJsx() { return ( - + - + ); } @@ -71,12 +74,13 @@ const numberOfActiveOrdersLimit = 'stop-order-warning-limit'; const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id); -const mockDataProvider = jest.fn((...args) => ({ +const mockDataProvider = jest.fn(() => ({ data: Array(0), reload: jest.fn(), })); jest.mock('@vegaprotocol/data-provider', () => ({ ...jest.requireActual('@vegaprotocol/data-provider'), + // @ts-ignore doesn't like spreading args here useDataProvider: jest.fn((...args) => mockDataProvider(...args)), })); @@ -84,11 +88,13 @@ describe('StopOrder', () => { beforeEach(() => { localStorage.clear(); useFeatureFlags.setState({ flags: { STOP_ORDERS: true } }); + mockConfig.store.setState({ pubKey: '0x123' }); }); afterEach(() => { localStorage.clear(); jest.clearAllMocks(); + mockConfig.reset(); }); it('should display ticket defaults limit order', async () => { @@ -250,7 +256,8 @@ describe('StopOrder', () => { }); it('does not submit if no wallet connected', async () => { - render(generateJsx(null)); + mockConfig.store.setState({ pubKey: undefined }); + render(generateJsx()); await userEvent.type(screen.getByTestId(sizeInput), '1'); await userEvent.type(screen.getByTestId(priceInput), '1'); await userEvent.type(screen.getByTestId(triggerPriceInput), '1'); @@ -550,9 +557,9 @@ describe('StopOrder', () => { data: Array(4), }); render(generateJsx()); - expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(true); + expect((mockDataProvider as jest.Mock).mock.lastCall?.[0].skip).toBe(true); await userEvent.type(screen.getByTestId(sizeInput), '0.01'); - expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(false); + expect((mockDataProvider as jest.Mock).mock.lastCall?.[0].skip).toBe(false); // 7002-SORD-011 expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument(); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx index f13c4d181..e82b38929 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.tsx @@ -1,5 +1,5 @@ import { useRef, useCallback, useEffect } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { type OrderSubmissionBody, type StopOrdersSubmission, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 8a3fb028c..f2f4f8249 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { act, fireEvent, @@ -24,6 +23,10 @@ import { formatForInput } from '@vegaprotocol/utils'; import type { PartialDeep } from 'type-fest'; import type { Market } from '@vegaprotocol/markets'; import type { MarketData } from '@vegaprotocol/markets'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; jest.mock('zustand'); jest.mock('./deal-ticket-fee-details', () => ({ @@ -56,7 +59,7 @@ function generateJsx( return ( - + - + ); } @@ -73,12 +76,19 @@ describe('DealTicket', () => { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); + mockConfig.store.setState({ pubKey }); + }); + + afterEach(() => { + act(() => { + mockConfig.reset(); + }); }); it('check filtering of active orders', async () => { const mockOrders: OrdersQuery = { party: { - id: 'pubKey', + id: pubKey, ordersConnection: { edges: [ { diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 04a9cce76..5d6fd8c7d 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -9,7 +9,7 @@ import { SideSelector } from './side-selector'; import { TimeInForceSelector } from './time-in-force-selector'; import { TypeSelector } from './type-selector'; import { type OrderSubmission } from '@vegaprotocol/wallet'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission'; import { TradingInput as Input, @@ -780,7 +780,7 @@ interface SummaryMessageProps { balance: string; margin: string; isReadOnly: boolean; - pubKey: string | null; + pubKey: string | undefined; onDeposit: (assetId: string) => void; } diff --git a/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx index 0db80f69f..8310df974 100644 --- a/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/margin-mode-selector.tsx @@ -8,7 +8,8 @@ import { Notification, Intent, } from '@vegaprotocol/ui-toolkit'; -import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet'; +import { MarginMode } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import * as Types from '@vegaprotocol/types'; import { type VegaTransactionStore, @@ -39,7 +40,7 @@ export const MarginChange = ({ marginMode, marginFactor, }: { - partyId: string | null; + partyId: string | undefined; marketId: string; marginMode: Types.MarginMode; marginFactor: string; diff --git a/libs/deal-ticket/src/hooks/use-estimate-fees.spec.tsx b/libs/deal-ticket/src/hooks/use-estimate-fees.spec.tsx index 55516e7c1..8afc0e266 100644 --- a/libs/deal-ticket/src/hooks/use-estimate-fees.spec.tsx +++ b/libs/deal-ticket/src/hooks/use-estimate-fees.spec.tsx @@ -55,7 +55,7 @@ jest.mock('./__generated__/EstimateOrder', () => ({ useEstimateFeesQuery: jest.fn((...args) => mockUseEstimateFeesQuery(...args)), })); -jest.mock('@vegaprotocol/wallet', () => ({ +jest.mock('@vegaprotocol/wallet-react', () => ({ useVegaWallet: () => ({ pubKey: 'pubKey' }), })); diff --git a/libs/deal-ticket/src/hooks/use-estimate-fees.tsx b/libs/deal-ticket/src/hooks/use-estimate-fees.tsx index 5f7c227b8..59ade8721 100644 --- a/libs/deal-ticket/src/hooks/use-estimate-fees.tsx +++ b/libs/deal-ticket/src/hooks/use-estimate-fees.tsx @@ -1,4 +1,4 @@ -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { EstimateFeesQuery } from './__generated__/EstimateOrder'; diff --git a/libs/deposits/src/lib/deposit-form.spec.tsx b/libs/deposits/src/lib/deposit-form.spec.tsx index f23d67657..b06dd4c7b 100644 --- a/libs/deposits/src/lib/deposit-form.spec.tsx +++ b/libs/deposits/src/lib/deposit-form.spec.tsx @@ -4,14 +4,14 @@ import BigNumber from 'bignumber.js'; import type { DepositFormProps } from './deposit-form'; import { DepositForm } from './deposit-form'; import * as Schema from '@vegaprotocol/types'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useWeb3ConnectStore } from '@vegaprotocol/web3'; import { useWeb3React } from '@web3-react/core'; import type { AssetFieldsFragment } from '@vegaprotocol/assets'; import type { DepositBalances } from './use-deposit-balances'; import { truncateMiddle } from '@vegaprotocol/ui-toolkit'; -jest.mock('@vegaprotocol/wallet'); +jest.mock('@vegaprotocol/wallet-react'); jest.mock('@vegaprotocol/web3'); jest.mock('@web3-react/core'); diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index d631e8c4f..ebf21ae1c 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -23,7 +23,7 @@ import { truncateMiddle, TradingButton, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useWeb3React } from '@web3-react/core'; import BigNumber from 'bignumber.js'; import type { ButtonHTMLAttributes, ChangeEvent, ReactNode } from 'react'; diff --git a/libs/environment/src/chains.ts b/libs/environment/src/chains.ts new file mode 100644 index 000000000..ce6fb46ff --- /dev/null +++ b/libs/environment/src/chains.ts @@ -0,0 +1,13 @@ +import { Networks } from './types'; + +export const CHAIN_IDS: { + [N in Networks]: string; +} = { + [Networks.DEVNET]: 'vega-devnet1-202402161135', + [Networks.STAGNET1]: 'vega-stagnet1-202307191148', + [Networks.TESTNET]: 'vega-fairground-202305051805', + [Networks.MAINNET_MIRROR]: '', + [Networks.VALIDATOR_TESTNET]: 'testnet-001', + [Networks.CUSTOM]: 'vega-stagnet1-202307191148', + [Networks.MAINNET]: 'vega-mainnet-0011', +}; diff --git a/libs/environment/src/index.ts b/libs/environment/src/index.ts index fa6986ab0..8d88d9835 100644 --- a/libs/environment/src/index.ts +++ b/libs/environment/src/index.ts @@ -9,3 +9,6 @@ export * from './types'; // Utils export * from './utils/__generated__/NodeCheck'; + +// Chain info +export { CHAIN_IDS } from './chains'; diff --git a/libs/environment/src/types.ts b/libs/environment/src/types.ts index a94d7aa0a..b68a89119 100644 --- a/libs/environment/src/types.ts +++ b/libs/environment/src/types.ts @@ -14,6 +14,7 @@ export enum Networks { DEVNET = 'DEVNET', MAINNET = 'MAINNET', } + export type Environment = z.infer; export type FeatureFlags = z.infer; export type CosmicElevatorFlags = Pick< diff --git a/libs/environment/src/utils/validate-environment.ts b/libs/environment/src/utils/validate-environment.ts index 0fc19e90a..07c9710e2 100644 --- a/libs/environment/src/utils/validate-environment.ts +++ b/libs/environment/src/utils/validate-environment.ts @@ -1,14 +1,5 @@ import z from 'zod'; - -export enum Networks { - VALIDATOR_TESTNET = 'VALIDATOR_TESTNET', - MAINNET_MIRROR = 'MAINNET_MIRROR', - CUSTOM = 'CUSTOM', - TESTNET = 'TESTNET', - STAGNET1 = 'STAGNET1', - DEVNET = 'DEVNET', - MAINNET = 'MAINNET', -} +import { Networks } from '../types'; // combine schema above with custom rule to ensure either // VEGA_URL or VEGA_CONFIG_URL are provided diff --git a/libs/funding-payments/src/setup-tests.ts b/libs/funding-payments/src/setup-tests.ts index 68773380a..0334ac6c9 100644 --- a/libs/funding-payments/src/setup-tests.ts +++ b/libs/funding-payments/src/setup-tests.ts @@ -1,4 +1,17 @@ import '@testing-library/jest-dom'; import ResizeObserver from 'resize-observer-polyfill'; +import { locales } from '@vegaprotocol/i18n'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// Set up i18n instance so that components have the correct default +// en translations +i18n.use(initReactI18next).init({ + // we init with resources + resources: locales, + fallbackLng: 'en', + ns: ['funding-payments'], + defaultNS: 'funding-payments', +}); global.ResizeObserver = ResizeObserver; diff --git a/libs/i18n/src/index.ts b/libs/i18n/src/index.ts index 22d76beaa..ad763dae3 100644 --- a/libs/i18n/src/index.ts +++ b/libs/i18n/src/index.ts @@ -19,6 +19,7 @@ import en_positions from './locales/en/positions.json'; import en_trades from './locales/en/trading.json'; import en_ui_toolkit from './locales/en/ui-toolkit.json'; import en_wallet from './locales/en/wallet.json'; +import en_wallet_react from './locales/en/wallet-react.json'; export const locales = { en: { @@ -41,5 +42,6 @@ export const locales = { trades: en_trades, 'ui-toolkit': en_ui_toolkit, wallet: en_wallet, + wallet_react: en_wallet_react, }, }; diff --git a/libs/i18n/src/locales/en/governance.json b/libs/i18n/src/locales/en/governance.json index e540cefaf..222651edd 100644 --- a/libs/i18n/src/locales/en/governance.json +++ b/libs/i18n/src/locales/en/governance.json @@ -158,6 +158,7 @@ "Continue": "Continue", "ContinueSharingData": "Continue sharing data", "copied!": "Copied!", + "Copy": "Copy", "copyToClipboard": "Copy to clipboard", "copyId": "Copy ID to clipboard", "CouldNotInstantiateMarket": "Could not instantiate market", @@ -193,6 +194,7 @@ "disclaimer4": "The Vega Governance App is provided “as is”. The developers of the Vega Governance App make no representations or warranties of any kind, whether express or implied, statutory or otherwise regarding the Vega Governance App. They disclaim all warranties of merchantability, quality, fitness for purpose. They disclaim all warranties that the Vega Governance App is free of harmful components or errors.", "disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.", "disconnect": "Disconnect", + "Disconnect all keys": "Disconnect all keys", "disconnectedNotice": "You have been disconnected. Connect your ETH wallet to the {{correctNetwork}} network to use this app.", "Dissociate VEGA tokens": "Disassociate $VEGA tokens", "Dissociating {{amount}} VEGA tokens from Vega key {{vegaKey}}": "Disassociating {{amount}} $VEGA tokens from Vega key {{vegaKey}}", @@ -239,7 +241,6 @@ "fully redeemable": "Tokens in this tranche have fully unlocked and can be redeemed once claimed.", "Fully unlocked": "Fully unlocked", "Fully vested on": "Fully vested on {{date}}", - "Get a Vega wallet": "Get a Vega Wallet", "getWallet": "Don't have a Vega wallet yet?", "getWalletLink": "Get a Vega wallet", "Go to staking or governance to see how you can use your unlocked tokens": "Go to staking or governance to see how you can use your unlocked tokens", @@ -632,6 +633,7 @@ "Score": "Score", "seeHowRewardsAreCalculated": "See how rewards are calculated", "seeRejectedProposals": "See rejected proposals", + "Select": "Select", "Select country": "Select country/region of residence", "Select your country or region of current residence": "Select your country or region of current residence", "SelectAMarketToChange": "Select a market to change", diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index f7eb93483..b2dc1a8d5 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -4,7 +4,6 @@ "(Tier {{tier}} as of last epoch)": "(Tier {{tier}} as of last epoch)", "24h vol": "24h vol", "24h volume": "24h volume", - "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a <1>custom wallet location": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a <1>custom wallet location", "A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer", "A successor to this market has been proposed": "A successor to this market has been proposed", "As a team creator, you cannot switch teams": "As a team creator, you cannot switch teams", @@ -15,7 +14,6 @@ "All teams": "All teams", "All teams are eligible": "All teams are eligible", "Amount earned": "Amount earned", - "An unknown error occurred.": "An unknown error occurred.", "Anonymous": "Anonymous", "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction": "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction", "Assessed over": "Assessed over", @@ -128,6 +126,7 @@ "Generate code": "Generate code", "Get rewards for providing liquidity.": "Get rewards for providing liquidity.", "Get started": "Get started", + "Get the Vega Wallet": "Get the Vega Wallet", "Give Feedback": "Give Feedback", "Go back and try again": "Go back and try again", "Got to competitions": "Go to competitions", @@ -382,7 +381,7 @@ "Vesting multiplier": "Vesting multiplier", "Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}", "Vesting {{vesting}}x": "Vesting {{vesting}}x", - "View as party": "View as party", + "View as public key": "View as public key", "View liquidity provision table": "View liquidity provision table", "View on Explorer": "View on Explorer", "View oracle specification": "View oracle specification", diff --git a/libs/i18n/src/locales/en/wallet-react.json b/libs/i18n/src/locales/en/wallet-react.json new file mode 100644 index 000000000..138146e57 --- /dev/null +++ b/libs/i18n/src/locales/en/wallet-react.json @@ -0,0 +1,13 @@ +{ + "Approve the connection from your wallet app.": "Approve the connection from your wallet app.", + "Cancel": "Cancel", + "Connect to Vega": "Connect to Vega", + "Connecting...": "Connecting...", + "Don't have a wallet?": "Don't have a wallet?", + "Get MetaMask": "Get MetaMask", + "Get the Vega Wallet": "Get the Vega Wallet", + "I agree": "I agree", + "Successfully connected": "Successfully connected", + "Transaction was not successful": "Transaction was not successful", + "Wallet rejected transaction": "Wallet rejected transaction" +} diff --git a/libs/i18n/src/locales/en/wallet.json b/libs/i18n/src/locales/en/wallet.json index 073f373c7..0967ef424 100644 --- a/libs/i18n/src/locales/en/wallet.json +++ b/libs/i18n/src/locales/en/wallet.json @@ -1,66 +1 @@ -{ - "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a <1>custom wallet location": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a <1>custom wallet location", - "About the Vega wallet": "About the Vega wallet", - "Advanced / Other options...": "Advanced / Other options...", - "An unknown error occurred": "An unknown error occurred", - "Approve the connection from your Vega wallet app.": "Approve the connection from your Vega wallet app.", - "Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with.": "Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with.", - "Browse from the perspective of another Vega user in read-only mode.": "Browse from the perspective of another Vega user in read-only mode.", - "Browse network": "Browse network", - "Cancel": "Cancel", - "Checking wallet version": "Checking wallet version", - "Checking your wallet is compatible with this app": "Checking your wallet is compatible with this app", - "Connect": "Connect", - "Connect directly via Metamask with the Vega Snap for single key support without advanced features.": "Connect directly via Metamask with the Vega Snap for single key support without advanced features.", - "Connect securely, deposit funds and approve or reject transactions with the Vega wallet": "Connect securely, deposit funds and approve or reject transactions with the Vega wallet", - "Connect the App/CLI": "Connect the App/CLI", - "Connect Vega wallet": "Connect Vega wallet", - "Connect via Vega MetaMask Snap": "Connect via Vega MetaMask Snap", - "Connect with Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.": "Connect with Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.", - "Connecting...": "Connecting...", - "Connection in progress": "Connection in progress", - "Copy": "Copy", - "Could not connect to Vega MetaMask Snap": "Could not connect to Vega MetaMask Snap", - "Custom wallet location": "Custom wallet location", - "Disconnect all keys": "Disconnect all keys", - "Enter a custom wallet location": "Enter a custom wallet location", - "Get a Vega wallet": "Get a Vega wallet", - "Get the Vega Wallet": "Get the Vega Wallet", - "Go back": "Go back", - "I agree": "I agree", - "Install Metamask with the Vega Snap for single key support without advanced features.": "Install Metamask with the Vega Snap for single key support without advanced features.", - "Install Vega MetaMask Snap": "Install Vega MetaMask Snap", - "Install Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.": "Install Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.", - "Metamask Snap <0>quick start": "Metamask Snap <0>quick start", - "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps": "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps", - "No Vega Wallet application running": "No Vega Wallet application running", - "No wallet application running at {{connectorUrl}}": "No wallet application running at {{connectorUrl}}", - "No wallet detected": "No wallet detected", - "Pubkey must be 64 characters in length": "Pubkey must be 64 characters in length", - "Pubkey must be be valid hex": "Pubkey must be be valid hex", - "Read the docs to troubleshoot": "Read the docs to troubleshoot", - "Required": "Required", - "Select": "Select", - "SELECT A VEGA KEY": "SELECT A VEGA KEY", - "Snap failed": "Snap failed", - "Something went wrong": "Something went wrong", - "Successfully connected": "Successfully connected", - "Supported browsers": "Supported browsers", - "The user rejected the wallet connection": "The user rejected the wallet connection", - "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".": "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".", - "Transaction could not be sent": "Transaction could not be sent", - "Transaction was not successful": "Transaction was not successful", - "Try again": "Try again", - "Understand the risk": "Understand the risk", - "Use the Desktop App/CLI": "Use the Desktop App/CLI", - "User rejected": "User rejected", - "Vega browser extension not installed": "Vega browser extension not installed", - "Vega Wallet <0>full featured": "Vega Wallet <0>full featured", - "Verifying chain": "Verifying chain", - "View as party": "View as party", - "VIEW AS VEGA USER": "VIEW AS VEGA USER", - "Wallet rejected transaction": "Wallet rejected transaction", - "Wrong Network": "Wrong Network", - "Wrong network": "Wrong network", - "your browser": "your browser" -} +{} diff --git a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx index b3e7bc2ec..9d2271061 100644 --- a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx +++ b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx @@ -1,5 +1,5 @@ import { TradingButton } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaTransactionStore } from '@vegaprotocol/web3'; import { useHasAmendableOrder } from '../../order-hooks'; import { useT } from '../../use-t'; diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index 8463c39b4..176538247 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -3,22 +3,32 @@ import type { OrderListManagerProps } from './order-list-manager'; import { OrderListManager, Filter } from './order-list-manager'; import * as useDataProviderHook from '@vegaprotocol/data-provider'; import type { OrderFieldsFragment } from '../'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; - -const generateJsx = (props: Partial | null = null) => { - const pubKey = '0x123'; - return ( - - - - - - ); -}; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; describe('OrderListManager', () => { + const pubKey = '0x123'; + + const generateJsx = (props: Partial | null = null) => { + return ( + + + + + + ); + }; + + beforeAll(() => { + mockConfig.store.setState({ pubKey }); + }); + + afterAll(() => { + mockConfig.reset(); + }); it('should render the order list if orders provided', async () => { jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ data: [{ id: '1' } as OrderFieldsFragment], @@ -32,7 +42,7 @@ describe('OrderListManager', () => { render(generateJsx()); expect( - await screen.getByRole('treegrid', { + await screen.findByRole('treegrid', { name: (_, element) => element.classList.contains('ag-root'), }) ).toBeInTheDocument(); @@ -52,7 +62,7 @@ describe('OrderListManager', () => { render(generateJsx()); expect( - await screen.getByText('Something went wrong: Query error') + await screen.findByText('Something went wrong: Query error') ).toBeInTheDocument(); }); diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index aa5078f5a..8d700a804 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -2,9 +2,6 @@ import { act, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { getDateTimeFormat } from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; -import type { PartialDeep } from 'type-fest'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; import type { Order, OrderFieldsFragment, OrderListTableProps } from '../'; import { OrderListTable } from '../'; @@ -13,6 +10,10 @@ import { limitOrder, marketOrder, } from '../mocks/generate-orders'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; // Mock theme switcher to get around inconsistent mocking of zustand // stores @@ -31,20 +32,25 @@ const defaultProps: OrderListTableProps = { isReadOnly: false, }; -const generateJsx = ( - props: Partial = defaultProps, - context: PartialDeep = { pubKey: '0x123' } -) => { +const generateJsx = (props: Partial = defaultProps) => { return ( - + - + ); }; describe('OrderListTable', () => { + beforeAll(() => { + mockConfig.store.setState({ pubKey: '0x123' }); + }); + + afterAll(() => { + mockConfig.reset(); + }); + it('should render correct columns', async () => { // 7003-MORD-001 // 7003-MORD-002 diff --git a/libs/orders/src/lib/components/stop-orders-manager/stop-orders-manager.spec.tsx b/libs/orders/src/lib/components/stop-orders-manager/stop-orders-manager.spec.tsx index b580004ae..bb4002e19 100644 --- a/libs/orders/src/lib/components/stop-orders-manager/stop-orders-manager.spec.tsx +++ b/libs/orders/src/lib/components/stop-orders-manager/stop-orders-manager.spec.tsx @@ -2,26 +2,37 @@ import { render, screen, act } from '@testing-library/react'; import { StopOrdersManager } from './stop-orders-manager'; import * as useDataProviderHook from '@vegaprotocol/data-provider'; import type { StopOrder } from '../order-data-provider/stop-orders-data-provider'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; jest.mock('../stop-orders-table/stop-orders-table', () => ({ StopOrdersTable: () =>
    StopOrdersTable
    , })); +const pubKey = '0x123'; + const generateJsx = () => { - const pubKey = '0x123'; return ( - + - + ); }; describe('StopOrdersManager', () => { + beforeAll(() => { + mockConfig.store.setState({ pubKey }); + }); + + afterAll(() => { + mockConfig.reset(); + }); + it('should render the stop orders table if data provided', async () => { jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ data: [{ id: '1' } as StopOrder], diff --git a/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.spec.tsx b/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.spec.tsx index 8059d26ba..a95793c7e 100644 --- a/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.spec.tsx +++ b/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.spec.tsx @@ -1,8 +1,5 @@ import { act, render, screen } from '@testing-library/react'; import * as Schema from '@vegaprotocol/types'; -import type { PartialDeep } from 'type-fest'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; import userEvent from '@testing-library/user-event'; import { @@ -10,6 +7,10 @@ import { type StopOrdersTableProps, } from './stop-orders-table'; import { generateStopOrder } from '../mocks/generate-stop-orders'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; // Mock theme switcher to get around inconsistent mocking of zustand // stores @@ -34,15 +35,12 @@ const defaultProps: StopOrdersTableProps = { isReadOnly: false, }; -const generateJsx = ( - props: Partial = defaultProps, - context: PartialDeep = { pubKey: '0x123' } -) => { +const generateJsx = (props: Partial = defaultProps) => { return ( - + - + ); }; @@ -111,6 +109,14 @@ const rowData = [ ]; describe('StopOrdersTable', () => { + beforeAll(() => { + mockConfig.store.setState({ pubKey: '0x123' }); + }); + + afterAll(() => { + mockConfig.reset(); + }); + it('should render correct columns', async () => { await act(async () => { render(generateJsx({ rowData })); diff --git a/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts b/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts index 321d61f34..97e83e806 100644 --- a/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts +++ b/libs/orders/src/lib/order-hooks/use-has-amendable-order.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; import { hasAmendableOrderProvider } from '../components/order-data-provider'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; export const useHasAmendableOrder = () => { diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index c3ac221bb..cf0931074 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -468,7 +468,10 @@ export const maxLeverageProvider = makeDerivedDataProvider< } ); -export const useMaxLeverage = (marketId: string, partyId: string | null) => { +export const useMaxLeverage = ( + marketId: string, + partyId: string | undefined +) => { return useDataProvider({ dataProvider: partyId ? maxLeverageProvider : maxMarketLeverageProvider, variables: { marketId, partyId: partyId || '' }, diff --git a/libs/positions/src/lib/positions-manager.spec.tsx b/libs/positions/src/lib/positions-manager.spec.tsx index a320eba42..54810f321 100644 --- a/libs/positions/src/lib/positions-manager.spec.tsx +++ b/libs/positions/src/lib/positions-manager.spec.tsx @@ -1,17 +1,21 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { PositionsManager } from './positions-manager'; import { positionsMarketsProvider } from './positions-data-providers'; import { singleRow } from './positions.mock'; import { MockedProvider } from '@apollo/client/testing'; import { HALFMAXGOINT64 } from '@vegaprotocol/utils'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; +import userEvent from '@testing-library/user-event'; +const key = { + publicKey: '123', + name: 'foo', +}; const mockCreate = jest.fn(); -jest.mock('@vegaprotocol/wallet', () => ({ - ...jest.requireActual('@vegaprotocol/wallet'), - useVegaWallet: jest.fn(() => ({ pubKey: 'partyId' })), -})); - jest.mock('@vegaprotocol/web3', () => ({ ...jest.requireActual('@vegaprotocol/web3'), useVegaTransactionStore: jest.fn(() => mockCreate), @@ -22,7 +26,7 @@ const mockUseDataProvider = (args: any) => { if (args.dataProvider === positionsMarketsProvider) { return { data: ['market-1', 'market-2'] }; } - return { data: [singleRow] }; + return { data: [{ ...singleRow, partyId: key.publicKey }] }; }; jest.mock('@vegaprotocol/data-provider', () => ({ @@ -31,13 +35,30 @@ jest.mock('@vegaprotocol/data-provider', () => ({ })); describe('PositionsManager', () => { + afterEach(() => { + mockConfig.reset(); + }); + it('should close position with half of max uint64', async () => { - render(, { - wrapper: MockedProvider, + const user = userEvent.setup(); + + mockConfig.store.setState({ + pubKey: key.publicKey, + keys: [key], + status: 'connected', }); + + render( + + + + + + ); + expect(await screen.findByTestId('close-position')).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('close-position')); + await user.click(screen.getByTestId('close-position')); expect( mockCreate.mock.lastCall[0].batchMarketInstructions.submissions[0].size diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index f78baa89c..201564dd4 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -1,5 +1,5 @@ import { PositionsTable } from './positions-table'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { positionsMetricsProvider, diff --git a/libs/positions/src/lib/use-market-margin.tsx b/libs/positions/src/lib/use-market-margin.tsx index 1eb4b6bdf..14300d08f 100644 --- a/libs/positions/src/lib/use-market-margin.tsx +++ b/libs/positions/src/lib/use-market-margin.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { marginsDataProvider } from '@vegaprotocol/accounts'; import type { MarginFieldsFragment } from '@vegaprotocol/accounts'; diff --git a/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.spec.tsx b/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.spec.tsx index fe9a25fe0..1880e762c 100644 --- a/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.spec.tsx +++ b/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.spec.tsx @@ -1,12 +1,11 @@ import { render, screen } from '@testing-library/react'; -import { WalletError } from '@vegaprotocol/wallet'; -import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaTxStatus } from '../../lib/proposals-hooks/use-vega-transaction'; import type { VegaTransactionDialogProps } from './vega-transaction-dialog'; import { VegaTransactionDialog } from './vega-transaction-dialog'; +import { unknownError } from '@vegaprotocol/wallet'; jest.mock('@vegaprotocol/environment', () => ({ + ...jest.requireActual('@vegaprotocol/environment'), useEnvironment: () => ({ VEGA_EXPLORER_URL: 'https://test.explorer.vega.network', VEGA_ENV: 'TESTNET', @@ -18,12 +17,6 @@ jest.mock('@vegaprotocol/environment', () => ({ })); describe('VegaTransactionDialog', () => { - const walletContext = { - network: 'TESTNET', - links: { - explorer: 'explorer', - }, - } as VegaWalletContextShape; let props: VegaTransactionDialogProps; beforeEach(() => { @@ -41,11 +34,7 @@ describe('VegaTransactionDialog', () => { }); it('requested', () => { - render( - - - - ); + render(); expect(screen.getByTestId('dialog-title')).toHaveTextContent(/confirm/i); expect(screen.getByTestId(VegaTxStatus.Requested)).toHaveTextContent( /please open your wallet/i @@ -57,16 +46,14 @@ describe('VegaTransactionDialog', () => { it('pending', () => { render( - - - + ); expect(screen.getByTestId('dialog-title')).toHaveTextContent(/awaiting/i); expect(screen.getByTestId(VegaTxStatus.Pending)).toHaveTextContent( @@ -77,35 +64,31 @@ describe('VegaTransactionDialog', () => { it('error', () => { render( - - - + ); expect(screen.getByTestId('dialog-title')).toHaveTextContent(/failed/i); expect(screen.getByTestId(VegaTxStatus.Error)).toHaveTextContent( - /rejected/i + /unknown error/i ); }); it('default complete', () => { render( - - - + ); expect(screen.getByTestId('dialog-title')).toHaveTextContent(/complete/i); expect(screen.getByTestId(VegaTxStatus.Complete)).toHaveTextContent( @@ -123,18 +106,16 @@ describe('VegaTransactionDialog', () => { [status]:
    {text}
    , }; render( - - - + ); expect(screen.getByTestId('dialog-title')).toHaveTextContent(title); expect(screen.getByText(text)).toBeInTheDocument(); @@ -147,7 +128,7 @@ describe('VegaTransactionDialog', () => { ); expect(screen.getByTestId('tx-block-explorer')).toHaveAttribute( 'href', - `${walletContext.links.explorer}/txs/0x${txHash}` + expect.stringContaining(`txs/${txHash}`) ); } }); diff --git a/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.tsx b/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.tsx index 3bcb5786c..759920a7f 100644 --- a/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.tsx +++ b/libs/proposals/src/components/vega-transaction-dialog/vega-transaction-dialog.tsx @@ -1,10 +1,14 @@ import type { ReactNode } from 'react'; import { Dialog, Icon, Intent, Loader } from '@vegaprotocol/ui-toolkit'; -import { WalletClientError } from '@vegaprotocol/wallet-client'; import type { VegaTxState } from '../../lib/proposals-hooks/use-vega-transaction'; import { VegaTxStatus } from '../../lib/proposals-hooks/use-vega-transaction'; -import { useVegaWallet } from '@vegaprotocol/wallet'; import { useT } from '../../use-t'; +import { + DApp, + EXPLORER_TX, + useEnvironment, + useLinks, +} from '@vegaprotocol/environment'; export type VegaTransactionContentMap = { [C in VegaTxStatus]?: JSX.Element; @@ -88,7 +92,8 @@ interface VegaDialogProps { */ export const VegaDialog = ({ transaction }: VegaDialogProps) => { const t = useT(); - const { links, network } = useVegaWallet(); + const { VEGA_ENV } = useEnvironment(); + const link = useLinks(DApp.Explorer); let content = null; if (transaction.status === VegaTxStatus.Requested) { @@ -99,9 +104,9 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => { 'Please open your wallet application and confirm or reject the transaction' )}

    - {network !== 'MAINNET' && ( + {VEGA_ENV !== 'MAINNET' && (

    - {t('[This is {{network}} transaction only]', { network })} + {t('[This is {{network}} transaction only]', { network: VEGA_ENV })}

    )} @@ -109,13 +114,12 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => { } if (transaction.status === VegaTxStatus.Error) { - let messageText = ''; - if (transaction.error instanceof WalletClientError) { - messageText = `${transaction.error.title}: ${transaction.error.message}`; - } else if (transaction.error instanceof Error) { - messageText = transaction.error.message; - } - content =
    {messageText}
    ; + content = ( +
    +

    {transaction.error?.message}

    +

    {transaction.error?.data}

    +
    + ); } if (transaction.status === VegaTxStatus.Pending) { @@ -127,7 +131,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => { @@ -148,7 +152,7 @@ export const VegaDialog = ({ transaction }: VegaDialogProps) => { diff --git a/libs/proposals/src/lib/proposals-hooks/use-proposal-submit.ts b/libs/proposals/src/lib/proposals-hooks/use-proposal-submit.ts index e6f844ee7..554837853 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-proposal-submit.ts +++ b/libs/proposals/src/lib/proposals-hooks/use-proposal-submit.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import * as Sentry from '@sentry/react'; -import { useVegaWallet, determineId } from '@vegaprotocol/wallet'; +import { determineId } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVegaTransaction } from './use-vega-transaction'; import { useProposalEvent } from './use-proposal-event'; import { type ProposalSubmission } from '@vegaprotocol/wallet'; @@ -9,7 +10,8 @@ import { type ProposalEventFieldsFragment } from './__generated__/Proposal'; export const useProposalSubmit = () => { const { pubKey } = useVegaWallet(); - const { send, transaction, setComplete, Dialog } = useVegaTransaction(); + const { send, transaction, setComplete, setTransaction } = + useVegaTransaction(); const waitForProposalEvent = useProposalEvent(transaction); const [finalizedProposal, setFinalizedProposal] = @@ -29,7 +31,12 @@ export const useProposalSubmit = () => { }); if (res) { + if ('error' in res) { + throw new Error('proposal failed'); + } + const proposalId = determineId(res.signature); + if (proposalId) { waitForProposalEvent(proposalId, pubKey, (p) => { setFinalizedProposal(p); @@ -47,7 +54,7 @@ export const useProposalSubmit = () => { return { transaction, finalizedProposal, - Dialog, submit, + setTransaction, }; }; diff --git a/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.spec.tsx b/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.spec.tsx index 3fb39b131..8b015b0e2 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.spec.tsx +++ b/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.spec.tsx @@ -1,36 +1,36 @@ import { act, renderHook } from '@testing-library/react'; -import type { VegaWalletContextShape, Transaction } from '@vegaprotocol/wallet'; -import { VegaWalletContext, WalletError } from '@vegaprotocol/wallet'; -import type { ReactNode } from 'react'; +import { + userRejectedError, + type Transaction, + ConnectorErrors, + sendTransactionError, +} from '@vegaprotocol/wallet'; import { initialState, useVegaTransaction, VegaTxStatus, } from './use-vega-transaction'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; +import { type ReactNode } from 'react'; const mockPubKey = '0x123'; -const defaultWalletContext = { - pubKey: null, - pubKeys: [], - isReadOnly: false, - sendTx: jest.fn(), - connect: jest.fn(), - disconnect: jest.fn(), - selectPubKey: jest.fn(), - connector: null, -} as unknown as VegaWalletContextShape; - -function setup(context?: Partial) { +function setup() { const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); return renderHook(() => useVegaTransaction(), { wrapper }); } describe('useVegaTransaction', () => { + const successObj = { + transactionHash: '0x123', + signature: 'signature', + }; + it('has the correct default state', () => { const { result } = setup(); expect(result.current).toEqual({ @@ -39,85 +39,58 @@ describe('useVegaTransaction', () => { reset: expect.any(Function), setComplete: expect.any(Function), setTransaction: expect.any(Function), - Dialog: expect.any(Function), }); }); it('resets state if sendTx returns null (user rejects)', async () => { - const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(null)); - const { result } = setup({ sendTx: mockSendTx }); + jest + .spyOn(mockConfig, 'sendTransaction') + .mockRejectedValue(userRejectedError()); + const { result } = setup(); await act(async () => { result.current.send(mockPubKey, {} as Transaction); }); expect(result.current.transaction.status).toEqual(VegaTxStatus.Default); }); - it('handles a single wallet error', () => { - const error = new WalletError('test error', 1, 'test data'); - const mockSendTx = jest.fn(() => { - throw error; - }); - const { result } = setup({ sendTx: mockSendTx }); - act(() => { - result.current.send(mockPubKey, {} as Transaction); - }); - expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); - expect(result.current.transaction.error).toHaveProperty( - 'message', - error.message - ); - expect(result.current.transaction.error).toHaveProperty('code', 1); - expect(result.current.transaction.error).toHaveProperty('data', error.data); - }); - it('handles a single error', () => { - const error = new Error('test error'); - const mockSendTx = jest.fn(() => { + const error = sendTransactionError(); + jest.spyOn(mockConfig, 'sendTransaction').mockImplementation(() => { throw error; }); - const { result } = setup({ sendTx: mockSendTx }); + const { result } = setup(); act(() => { result.current.send(mockPubKey, {} as Transaction); }); expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); expect(result.current.transaction.error).toHaveProperty( 'message', - error.message + ConnectorErrors.sendTransaction.message ); }); it('handles an unknown error', () => { const unknownThrow = { foo: 'bar' }; - const mockSendTx = jest.fn(() => { + jest.spyOn(mockConfig, 'sendTransaction').mockImplementation(() => { throw unknownThrow; }); - const { result } = setup({ sendTx: mockSendTx }); + const { result } = setup(); act(() => { result.current.send(mockPubKey, {} as Transaction); }); expect(result.current.transaction.status).toEqual(VegaTxStatus.Error); - expect(result.current.transaction.error).toHaveProperty( - 'title', - 'Something went wrong' - ); - expect(result.current.transaction.error).toHaveProperty('code', 105); expect(result.current.transaction.error).toHaveProperty( 'message', - 'Unknown error occurred' - ); - expect(result.current.transaction.error).toHaveProperty( - 'data', - 'Unknown error occurred' + ConnectorErrors.unknown.message ); }); it('sets txHash and signature to state if successful', async () => { - const successObj = { - transactionHash: '0x123', - signature: 'signature', - }; - const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(successObj)); - const { result } = setup({ sendTx: mockSendTx }); + jest + .spyOn(mockConfig, 'sendTransaction') + // @ts-ignore fields ommitted for brevity + .mockResolvedValue(successObj); + const { result } = setup(); await act(async () => { result.current.send(mockPubKey, {} as Transaction); }); @@ -137,14 +110,12 @@ describe('useVegaTransaction', () => { }); it('reset resets transaction status', async () => { - const mockSendTx = jest.fn().mockReturnValue( - Promise.resolve({ - transactionHash: '0x123', - signature: 'signature', - }) - ); + jest + .spyOn(mockConfig, 'sendTransaction') + // @ts-ignore fields ommitted for brevity + .mockResolvedValue(successObj); - const { result } = setup({ sendTx: mockSendTx }); + const { result } = setup(); await act(async () => { result.current.send(mockPubKey, {} as Transaction); diff --git a/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.tsx b/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.tsx index b87b81e9d..53bf8ddfa 100644 --- a/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.tsx +++ b/libs/proposals/src/lib/proposals-hooks/use-vega-transaction.tsx @@ -1,14 +1,11 @@ -import type { ReactNode } from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { - WalletClientError, - WalletHttpError, -} from '@vegaprotocol/wallet-client'; -import { useVegaWallet, WalletError, ClientErrors } from '@vegaprotocol/wallet'; -import type { Transaction } from '@vegaprotocol/wallet'; -import type { VegaTransactionContentMap } from '../../components/vega-transaction-dialog'; -import { VegaTransactionDialog } from '../../components/vega-transaction-dialog'; -import type { Intent } from '@vegaprotocol/ui-toolkit'; + ConnectorError, + ConnectorErrors, + unknownError, + type Transaction, +} from '@vegaprotocol/wallet'; export enum VegaTxStatus { Default = 'Default', @@ -20,19 +17,12 @@ export enum VegaTxStatus { export interface VegaTxState { status: VegaTxStatus; - error: Error | null; + error: ConnectorError | null; txHash: string | null; signature: string | null; dialogOpen: boolean; } -export interface DialogProps { - intent?: Intent; - title?: string; - icon?: ReactNode; - content?: VegaTransactionContentMap; -} - export const initialState = { status: VegaTxStatus.Default, error: null, @@ -41,21 +31,8 @@ export const initialState = { dialogOpen: false, }; -export const orderErrorResolve = (err: Error | unknown): Error => { - if (err instanceof WalletClientError || err instanceof WalletError) { - return err; - } else if (err instanceof WalletHttpError) { - return ClientErrors.UNKNOWN; - } else if (err instanceof TypeError) { - return ClientErrors.NO_SERVICE; - } else if (err instanceof Error) { - return err; - } - return ClientErrors.UNKNOWN; -}; - export const useVegaTransaction = () => { - const { sendTx, disconnect } = useVegaWallet(); + const { sendTx } = useVegaWallet(); const [transaction, _setTransaction] = useState(initialState); const setTransaction = useCallback((update: Partial) => { @@ -86,10 +63,8 @@ export const useVegaTransaction = () => { const res = await sendTx(pubKey, tx); - if (res === null) { - // User rejected - reset(); - return null; + if (!res) { + return; } if (res.signature && res.transactionHash) { @@ -104,40 +79,28 @@ export const useVegaTransaction = () => { return null; } catch (err) { - const error = orderErrorResolve(err); - if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) { - disconnect(); + if ( + err instanceof ConnectorError && + err.code === ConnectorErrors.userRejected.code + ) { + reset(); + return; } setTransaction({ - error, + error: err instanceof ConnectorError ? err : unknownError(), status: VegaTxStatus.Error, }); return null; } }, - [sendTx, setTransaction, reset, disconnect] + [sendTx, setTransaction, reset] ); - const Dialog = useMemo(() => { - return (props: DialogProps) => ( - { - if (!isOpen) reset(); - setTransaction({ dialogOpen: isOpen }); - }} - transaction={transaction} - /> - ); - }, [transaction, setTransaction, reset]); - return { send, transaction, reset, setComplete, setTransaction, - Dialog, }; }; diff --git a/libs/proposals/src/lib/voting-hooks/use-vote-submit.ts b/libs/proposals/src/lib/voting-hooks/use-vote-submit.ts index 375516336..a8a09c89b 100644 --- a/libs/proposals/src/lib/voting-hooks/use-vote-submit.ts +++ b/libs/proposals/src/lib/voting-hooks/use-vote-submit.ts @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import * as Sentry from '@sentry/react'; import { useVegaTransaction } from '../proposals-hooks/use-vega-transaction'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useVoteEvent } from './use-vote-event'; import type { VoteValue } from '@vegaprotocol/types'; import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption'; @@ -10,7 +10,7 @@ export type FinalizedVote = VoteEventFieldsFragment & { pubKey: string }; export const useVoteSubmit = () => { const { pubKey } = useVegaWallet(); - const { send, transaction, setComplete, Dialog } = useVegaTransaction(); + const { send, transaction, setComplete } = useVegaTransaction(); const waitForVoteEvent = useVoteEvent(transaction); const [finalizedVote, setFinalizedVote] = useState( @@ -49,7 +49,6 @@ export const useVoteSubmit = () => { return { transaction, finalizedVote, - Dialog, submit, }; }; diff --git a/libs/react-helpers/src/hooks/index.ts b/libs/react-helpers/src/hooks/index.ts index 186fc4016..9747c7773 100644 --- a/libs/react-helpers/src/hooks/index.ts +++ b/libs/react-helpers/src/hooks/index.ts @@ -13,3 +13,4 @@ export * from './use-storybook-theme-observer'; export * from './use-yesterday'; export * from './use-previous'; export { useScript } from './use-script'; +export { useUserAgent } from './use-user-agent'; diff --git a/libs/react-helpers/src/hooks/use-user-agent.ts b/libs/react-helpers/src/hooks/use-user-agent.ts new file mode 100644 index 000000000..e4b04d41f --- /dev/null +++ b/libs/react-helpers/src/hooks/use-user-agent.ts @@ -0,0 +1,12 @@ +type Browser = 'chrome' | 'firefox' | undefined; + +export const useUserAgent = (): Browser => { + const isItChrome = window.navigator.userAgent.includes('Chrome'); + const isItMozilla = + window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + + if (isItChrome) return 'chrome'; + if (isItMozilla) return 'firefox'; + + return undefined; +}; diff --git a/libs/ui-toolkit/src/components/button/button.tsx b/libs/ui-toolkit/src/components/button/button.tsx index 180dee0d1..39a04e2f5 100644 --- a/libs/ui-toolkit/src/components/button/button.tsx +++ b/libs/ui-toolkit/src/components/button/button.tsx @@ -140,7 +140,7 @@ type ButtonLinkProps = Omit, 'style'>; export const ButtonLink = forwardRef( ({ type = 'button', className, ...props }, ref) => { - const style = classnames('inline underline', className); + const style = classnames('inline underline underline-offset-4', className); return + ); +}); + +const ConnectionOptionLink = forwardRef< + HTMLAnchorElement, + { + children: ReactNode; + id: ConnectorType; + href: string; + } +>(({ children, id, href }, ref) => { + return ( + + + {children} + + ); +}); + +const ConnectionOptionButtonWithDescription = forwardRef< + HTMLButtonElement, + { + children: ReactNode; + id: ConnectorType; + onClick: () => void; + } +>(({ children, id, onClick }, ref) => { + return ( + + ); +}); + +const ConnectionOptionLinkWithDescription = forwardRef< + HTMLAnchorElement, + { + children: ReactNode; + id: ConnectorType; + href: string; + } +>(({ children, id, href }, ref) => { + return ( + + + + + {children} + + ); +}); + +export const ConnectionOptionRecord: { + [C in ConnectorType]?: FunctionComponent; +} = { + injected: ConnectionOptionInjected, + snap: ConnectionOptionSnap, +}; diff --git a/libs/wallet-react/src/components/connect-dialog/connection-status.tsx b/libs/wallet-react/src/components/connect-dialog/connection-status.tsx new file mode 100644 index 000000000..8741a548b --- /dev/null +++ b/libs/wallet-react/src/components/connect-dialog/connection-status.tsx @@ -0,0 +1,29 @@ +import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import { type Status } from '@vegaprotocol/wallet'; +import { useT } from '../../hooks/use-t'; + +export const ConnectionStatus = ({ status }: { status: Status }) => { + const t = useT(); + + if (status === 'connecting') { + return ( + <> +

    {t('Connecting...')}

    +

    + {t('Approve the connection from your wallet app.')} +

    + + ); + } + + if (status === 'connected') { + return ( +
    + +

    {t('Successfully connected')}

    +
    + ); + } + + return null; +}; diff --git a/libs/wallet-react/src/components/connect-dialog/connector-icon.tsx b/libs/wallet-react/src/components/connect-dialog/connector-icon.tsx new file mode 100644 index 000000000..47c0dda2d --- /dev/null +++ b/libs/wallet-react/src/components/connect-dialog/connector-icon.tsx @@ -0,0 +1,68 @@ +import classNames from 'classnames'; +import { type ConnectorType } from '@vegaprotocol/wallet'; +import { VegaIcon, VegaIconNames, VLogo } from '@vegaprotocol/ui-toolkit'; + +export const ConnectorIcon = ({ id }: { id: ConnectorType }) => { + const defaultWrapperClasses = + 'flex items-center justify-center w-8 h-8 rounded'; + switch (id) { + case 'injected': { + return ( + + + + ); + } + case 'jsonRpc': { + return ( + + {'>_'} + + ); + } + case 'snap': { + return ( + + + + ); + } + case 'viewParty': { + return ( + + + + ); + } + default: { + return ( + + ); + } + } +}; diff --git a/libs/wallet-react/src/components/connect-dialog/index.ts b/libs/wallet-react/src/components/connect-dialog/index.ts new file mode 100644 index 000000000..ffef21097 --- /dev/null +++ b/libs/wallet-react/src/components/connect-dialog/index.ts @@ -0,0 +1,8 @@ +export { ConnectDialogWithRiskAck, ConnectDialog } from './connect-dialog'; +export { + ConnectionOptions, + ConnectionOptionDefault, + ConnectionOptionRecord, +} from './connection-options'; +export { ConnectionStatus } from './connection-status'; +export { ConnectorIcon } from './connector-icon'; diff --git a/libs/wallet-react/src/components/risk-ack/index.ts b/libs/wallet-react/src/components/risk-ack/index.ts new file mode 100644 index 000000000..c11c58ae3 --- /dev/null +++ b/libs/wallet-react/src/components/risk-ack/index.ts @@ -0,0 +1 @@ +export { RiskAck } from './risk-ack'; diff --git a/libs/wallet-react/src/components/risk-ack/risk-ack.tsx b/libs/wallet-react/src/components/risk-ack/risk-ack.tsx new file mode 100644 index 000000000..bba06f05a --- /dev/null +++ b/libs/wallet-react/src/components/risk-ack/risk-ack.tsx @@ -0,0 +1,34 @@ +import { TradingButton as Button, Intent } from '@vegaprotocol/ui-toolkit'; +import { type ReactNode } from 'react'; +import { useT } from '../../hooks/use-t'; + +export const RiskAck = ({ + children, + onAccept, + onReject, +}: { + children: ReactNode; + onAccept: () => void; + onReject: () => void; +}) => { + const t = useT(); + + return ( +
    +

    {t('Understand the risk')}

    + {children} +
    +
    + +
    +
    + +
    +
    +
    + ); +}; diff --git a/libs/wallet-react/src/constants.ts b/libs/wallet-react/src/constants.ts new file mode 100644 index 000000000..a86c3b114 --- /dev/null +++ b/libs/wallet-react/src/constants.ts @@ -0,0 +1,11 @@ +export const Links = { + walletOverview: 'https://vega.xyz/wallet', + chromeExtension: + 'https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel', + mozillaExtension: + 'https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet', + chromeMetaMaskExtension: + 'https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn', + mozillaMetaMaskExtension: + 'https://addons.mozilla.org/firefox/addon/ether-metamask/', +} as const; diff --git a/libs/wallet-react/src/context.ts b/libs/wallet-react/src/context.ts new file mode 100644 index 000000000..791226390 --- /dev/null +++ b/libs/wallet-react/src/context.ts @@ -0,0 +1,11 @@ +import { createContext, createElement, type PropsWithChildren } from 'react'; +import { type Wallet } from '@vegaprotocol/wallet'; + +export const WalletContext = createContext(undefined); + +export function WalletProvider({ + children, + config, +}: PropsWithChildren<{ config: Wallet }>) { + return createElement(WalletContext.Provider, { value: config }, children); +} diff --git a/libs/wallet-react/src/hooks/use-chain-id.ts b/libs/wallet-react/src/hooks/use-chain-id.ts new file mode 100644 index 000000000..d22d662cb --- /dev/null +++ b/libs/wallet-react/src/hooks/use-chain-id.ts @@ -0,0 +1,6 @@ +import { useWallet } from './use-wallet'; + +export function useChainId() { + const chainId = useWallet((store) => store.chainId); + return { chainId }; +} diff --git a/libs/wallet-react/src/hooks/use-config.ts b/libs/wallet-react/src/hooks/use-config.ts new file mode 100644 index 000000000..db0c4ee52 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-config.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { WalletContext } from '../context'; + +export function useConfig() { + const context = useContext(WalletContext); + if (context === undefined) { + throw new Error('must be used within VegaWalletProvider'); + } + return context; +} diff --git a/libs/wallet-react/src/hooks/use-connect.ts b/libs/wallet-react/src/hooks/use-connect.ts new file mode 100644 index 000000000..4bdae0c1c --- /dev/null +++ b/libs/wallet-react/src/hooks/use-connect.ts @@ -0,0 +1,9 @@ +import { useConfig } from './use-config'; + +export function useConnect() { + const config = useConfig(); + return { + connectors: config.connectors, + connect: config.connect, + }; +} diff --git a/libs/wallet-react/src/hooks/use-connector.ts b/libs/wallet-react/src/hooks/use-connector.ts new file mode 100644 index 000000000..62ae87262 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-connector.ts @@ -0,0 +1,13 @@ +import { useConfig } from './use-config'; +import { useWallet } from './use-wallet'; + +export function useConnector() { + const config = useConfig(); + const current = useWallet((store) => store.current); + + const connector = config.connectors.find((c) => c.id === current); + + return { + connector, + }; +} diff --git a/libs/wallet-react/src/hooks/use-dialog-store.ts b/libs/wallet-react/src/hooks/use-dialog-store.ts new file mode 100644 index 000000000..4d6815550 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-dialog-store.ts @@ -0,0 +1,15 @@ +import { create } from 'zustand'; + +export interface DialogStore { + isOpen: boolean; + set: (open: boolean) => void; + open: () => void; + close: () => void; +} + +export const useDialogStore = create()((set) => ({ + isOpen: false, + set: (open: boolean) => set({ isOpen: open }), + open: () => set({ isOpen: true }), + close: () => set({ isOpen: false }), +})); diff --git a/libs/wallet-react/src/hooks/use-disconnect.ts b/libs/wallet-react/src/hooks/use-disconnect.ts new file mode 100644 index 000000000..ec841190b --- /dev/null +++ b/libs/wallet-react/src/hooks/use-disconnect.ts @@ -0,0 +1,8 @@ +import { useConfig } from './use-config'; + +export function useDisconnect() { + const config = useConfig(); + return { + disconnect: config.disconnect, + }; +} diff --git a/libs/wallet-react/src/hooks/use-eager-connect.ts b/libs/wallet-react/src/hooks/use-eager-connect.ts new file mode 100644 index 000000000..fa5d3e5c2 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-eager-connect.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import { useWallet } from './use-wallet'; +import { useConnect } from './use-connect'; + +export function useEagerConnect() { + const current = useWallet((store) => store.current); + 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; + } + + try { + await connect(current); + } catch { + console.warn(`Failed to connect with connector: ${current}`); + } finally { + setConnecting(false); + } + }; + + if (typeof window !== 'undefined') { + attemptConnect(); + } + }, [connect, current, connecting]); + + return connecting; +} diff --git a/libs/wallet-react/src/hooks/use-pub-keys.ts b/libs/wallet-react/src/hooks/use-pub-keys.ts new file mode 100644 index 000000000..65aa9d523 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-pub-keys.ts @@ -0,0 +1,8 @@ +import { useWallet } from './use-wallet'; + +export function usePubKeys() { + const keys = useWallet((store) => store.keys); + return { + pubKeys: keys, + }; +} diff --git a/libs/wallet-react/src/hooks/use-reconnect.ts b/libs/wallet-react/src/hooks/use-reconnect.ts new file mode 100644 index 000000000..1a302f028 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-reconnect.ts @@ -0,0 +1,12 @@ +import { useDialogStore } from './use-dialog-store'; +import { useDisconnect } from './use-disconnect'; + +export function useReconnect() { + const openDialog = useDialogStore((store) => store.open); + const { disconnect } = useDisconnect(); + + return async () => { + await disconnect(); + openDialog(); + }; +} diff --git a/libs/wallet-react/src/hooks/use-send-transaction.ts b/libs/wallet-react/src/hooks/use-send-transaction.ts new file mode 100644 index 000000000..ea36c0cdd --- /dev/null +++ b/libs/wallet-react/src/hooks/use-send-transaction.ts @@ -0,0 +1,8 @@ +import { useConfig } from './use-config'; + +export function useSendTransaction() { + const config = useConfig(); + return { + sendTransaction: config.sendTransaction, + }; +} diff --git a/libs/wallet/src/use-simple-transaction.ts b/libs/wallet-react/src/hooks/use-simple-transaction.ts similarity index 89% rename from libs/wallet/src/use-simple-transaction.ts rename to libs/wallet-react/src/hooks/use-simple-transaction.ts index ec41d76cc..299571cb5 100644 --- a/libs/wallet/src/use-simple-transaction.ts +++ b/libs/wallet-react/src/hooks/use-simple-transaction.ts @@ -1,12 +1,16 @@ import { useState } from 'react'; import { useVegaWallet } from './use-vega-wallet'; -import { type Transaction } from './connectors'; +import { + ConnectorError, + ConnectorErrors, + determineId, + type Transaction, +} from '@vegaprotocol/wallet'; import { useSimpleTransactionSubscription, type SimpleTransactionFieldsFragment, -} from './__generated__/SimpleTransaction'; +} from '../__generated__/SimpleTransaction'; import { useT } from './use-t'; -import { determineId } from './utils'; export type Status = 'idle' | 'requested' | 'pending' | 'confirmed'; @@ -44,10 +48,6 @@ export const useSimpleTransaction = (opts?: Options) => { try { const res = await sendTx(pubKey, tx); - if (!res) { - throw new Error(t('Transaction could not be sent')); - } - setStatus('pending'); setResult({ txHash: res?.transactionHash.toLowerCase(), @@ -55,8 +55,8 @@ export const useSimpleTransaction = (opts?: Options) => { id: determineId(res.signature), }); } catch (err) { - if (err instanceof Error) { - if (err.message.includes('user rejected')) { + if (err instanceof ConnectorError) { + if (err.code === ConnectorErrors.userRejected.code) { setStatus('idle'); } else { setError(err.message); diff --git a/libs/wallet/src/use-t.ts b/libs/wallet-react/src/hooks/use-t.ts similarity index 73% rename from libs/wallet/src/use-t.ts rename to libs/wallet-react/src/hooks/use-t.ts index 807f19656..9b8fd02b4 100644 --- a/libs/wallet/src/use-t.ts +++ b/libs/wallet-react/src/hooks/use-t.ts @@ -1,3 +1,3 @@ import { useTranslation } from 'react-i18next'; -export const ns = 'wallet'; +export const ns = 'wallet-react'; export const useT = () => useTranslation(ns).t; diff --git a/libs/wallet-react/src/hooks/use-vega-wallet.ts b/libs/wallet-react/src/hooks/use-vega-wallet.ts new file mode 100644 index 000000000..ba2e79d79 --- /dev/null +++ b/libs/wallet-react/src/hooks/use-vega-wallet.ts @@ -0,0 +1,26 @@ +import { type Transaction } from '@vegaprotocol/wallet'; +import { useConfig } from './use-config'; +import { useWallet } from './use-wallet'; + +// Only for vega apps that expect a single selected key +export const useVegaWallet = () => { + const config = useConfig(); + const store = useWallet((store) => store); + + return { + status: store.status, + pubKeys: store.keys, + pubKey: store.pubKey, + selectPubKey: (pubKey: string) => config.store.setState({ pubKey }), + isReadOnly: store.current === 'viewParty', + disconnect: config.disconnect, + refreshKeys: config.refreshKeys, + sendTx: (pubKey: string, transaction: Transaction) => { + return config.sendTransaction({ + publicKey: pubKey, + sendingMode: 'TYPE_SYNC', + transaction, + }); + }, + }; +}; diff --git a/libs/wallet-react/src/hooks/use-wallet.ts b/libs/wallet-react/src/hooks/use-wallet.ts new file mode 100644 index 000000000..710418fca --- /dev/null +++ b/libs/wallet-react/src/hooks/use-wallet.ts @@ -0,0 +1,10 @@ +import { useStore } from 'zustand'; +import { type Store } from '@vegaprotocol/wallet'; +import { useConfig } from './use-config'; + +export function useWallet(selector: (store: Store) => T) { + const config = useConfig(); + const store = useStore(config.store, selector); + + return store; +} diff --git a/libs/wallet-react/src/index.ts b/libs/wallet-react/src/index.ts new file mode 100644 index 000000000..72a5d26bd --- /dev/null +++ b/libs/wallet-react/src/index.ts @@ -0,0 +1,28 @@ +// Context +export { WalletContext, WalletProvider } from './context'; + +// Hooks +export { useChainId } from './hooks/use-chain-id'; +export { useDialogStore } from './hooks/use-dialog-store'; +export { useEagerConnect } from './hooks/use-eager-connect'; +export { + useSimpleTransaction, + type Options, + type Status, +} from './hooks/use-simple-transaction'; +export { useVegaWallet } from './hooks/use-vega-wallet'; +export { useConfig } from './hooks/use-config'; +export { useConnect } from './hooks/use-connect'; +export { useConnector } from './hooks/use-connector'; +export { useDisconnect } from './hooks/use-disconnect'; +export { usePubKeys } from './hooks/use-pub-keys'; +export { useReconnect } from './hooks/use-reconnect'; +export { useSendTransaction } from './hooks/use-send-transaction'; +export { useWallet } from './hooks/use-wallet'; + +// Constants +export { Links } from './constants'; + +// Components +export * from './components/connect-dialog'; +export { RiskAck } from './components/risk-ack'; diff --git a/libs/wallet-react/src/setup-tests.ts b/libs/wallet-react/src/setup-tests.ts new file mode 100644 index 000000000..73b4be0f2 --- /dev/null +++ b/libs/wallet-react/src/setup-tests.ts @@ -0,0 +1,14 @@ +import '@testing-library/jest-dom'; +import { locales } from '@vegaprotocol/i18n'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// Set up i18n instance so that components have the correct default +// en translations +i18n.use(initReactI18next).init({ + // we init with resources + resources: locales, + fallbackLng: 'en', + ns: ['wallet-react'], + defaultNS: 'wallet-react', +}); diff --git a/libs/wallet-react/src/testing.ts b/libs/wallet-react/src/testing.ts new file mode 100644 index 000000000..768e921e6 --- /dev/null +++ b/libs/wallet-react/src/testing.ts @@ -0,0 +1,29 @@ +import { createElement, type PropsWithChildren } from 'react'; +import { + createConfig, + mockChain, + MockConnector, + type Wallet, +} from '@vegaprotocol/wallet'; +import { WalletContext } from './context'; + +const mockConnector = new MockConnector(); + +export const mockConfig = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], +}); + +export function MockedWalletProvider({ + children, + config, +}: PropsWithChildren<{ config?: Wallet }>) { + return createElement( + WalletContext.Provider, + { + value: config ? config : mockConfig, + }, + children + ); +} diff --git a/libs/wallet-react/tsconfig.json b/libs/wallet-react/tsconfig.json new file mode 100644 index 000000000..3c41f10fb --- /dev/null +++ b/libs/wallet-react/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json" +} diff --git a/libs/wallet-react/tsconfig.lib.json b/libs/wallet-react/tsconfig.lib.json new file mode 100644 index 000000000..cfc484329 --- /dev/null +++ b/libs/wallet-react/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "node", + + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/wallet-react/tsconfig.spec.json b/libs/wallet-react/tsconfig.spec.json new file mode 100644 index 000000000..d345f9bc0 --- /dev/null +++ b/libs/wallet-react/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/wallet/project.json b/libs/wallet/project.json index 5b024dc38..273238344 100644 --- a/libs/wallet/project.json +++ b/libs/wallet/project.json @@ -17,12 +17,8 @@ "react", "react-dom", "react/jsx-runtime", - "@vegaprotocol/i18n", - "@vegaprotocol/react-helpers", "@vegaprotocol/types", - "@vegaprotocol/ui-toolkit", - "@vegaprotocol/utils", - "@vegaprotocol/wallet-client" + "@vegaprotocol/utils" ], "rollupConfig": "@nx/react/plugins/bundle-rollup", "compiler": "swc", diff --git a/libs/wallet/src/chains.ts b/libs/wallet/src/chains.ts new file mode 100644 index 000000000..9bec5d47a --- /dev/null +++ b/libs/wallet/src/chains.ts @@ -0,0 +1,29 @@ +export type Chain = { + id: string; + testnet: boolean; + name: string; +}; + +export const mainnet = { + id: 'vega-mainnet-0011', + testnet: false, + name: 'Fairground', +}; + +export const fairground = { + id: 'vega-fairground-202305051805', + testnet: true, + name: 'Fairground', +}; + +export const stagnet = { + id: 'vega-stagnet1-202307191148', + testnet: true, + name: 'Stagnet', +}; + +export const mockChain = { + id: 'mock-chain', + testnet: true, + name: 'My Mocked Chain', +}; diff --git a/libs/wallet/src/connect-dialog/chrome-icon.tsx b/libs/wallet/src/connect-dialog/chrome-icon.tsx deleted file mode 100644 index f51ec69c5..000000000 --- a/libs/wallet/src/connect-dialog/chrome-icon.tsx +++ /dev/null @@ -1,77 +0,0 @@ -export const ChromeIcon = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/libs/wallet/src/connect-dialog/connect-dialog-elements.tsx b/libs/wallet/src/connect-dialog/connect-dialog-elements.tsx deleted file mode 100644 index 72f791c5d..000000000 --- a/libs/wallet/src/connect-dialog/connect-dialog-elements.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - ExternalLink, - VegaIcon, - VegaIconNames, -} from '@vegaprotocol/ui-toolkit'; -import classNames from 'classnames'; -import type { ReactNode } from 'react'; -import { MozillaIcon } from './mozilla-icon'; -import { ChromeIcon } from './chrome-icon'; -import { useVegaWallet } from '../use-vega-wallet'; -import { useT } from '../use-t'; - -export const ConnectDialogTitle = ({ children }: { children: ReactNode }) => { - return ( -

    - {children} -

    - ); -}; - -export const ConnectDialogContent = ({ children }: { children: ReactNode }) => { - return
    {children}
    ; -}; - -export const ConnectDialogFooter = () => { - const t = useT(); - const { links } = useVegaWallet(); - const wrapperClasses = classNames( - 'flex justify-center gap-4 mt-4', - 'px-4 md:px-8 pt-4 md:pt-6', - 'border-t border-vega-light-200 dark:border-vega-dark-200', - 'text-vega-light-400 dark:text-vega-dark-400 text-sm' - ); - return ( -
    - - {t('About the Vega wallet')}{' '} - - - {' | '} - - {t('Supported browsers')}{' '} - - -
    - ); -}; - -export const BrowserIcon = ({ - chromeExtensionUrl, - mozillaExtensionUrl, -}: { - chromeExtensionUrl: string; - mozillaExtensionUrl: string; -}) => { - const isItChrome = window.navigator.userAgent.includes('Chrome'); - const isItMozilla = - window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - return ( -
    - {!isItChrome && !isItMozilla ? ( - <> - - - {' '} - - - - - ) : ( - <> - {isItChrome && } - {isItMozilla && } - - )} -
    - ); -}; diff --git a/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx b/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx deleted file mode 100644 index 4c84027d9..000000000 --- a/libs/wallet/src/connect-dialog/connect-dialog.spec.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import { act, fireEvent, render, screen } from '@testing-library/react'; -import type { VegaWalletConfig } from '../provider'; -import { VegaWalletProvider } from '../provider'; -import { VegaConnectDialog, CLOSE_DELAY } from './connect-dialog'; -import { useVegaWalletDialogStore } from './vega-wallet-dialog-store'; -import type { VegaConnectDialogProps } from '..'; -import { - ClientErrors, - InjectedConnector, - JsonRpcConnector, - ViewConnector, - WalletError, -} from '../connectors'; -import { - mockBrowserWallet, - clearBrowserWallet, - delayedReject, - delayedResolve, -} from '../test-helpers'; - -const mockUpdateDialogOpen = jest.fn(); -const mockCloseVegaDialog = jest.fn(); - -let mockIsDesktopRunning = true; -const mockChainId = 'VEGA_CHAIN_ID'; - -jest.mock('../use-is-wallet-service-running', () => ({ - useIsWalletServiceRunning: jest - .fn() - .mockImplementation(() => mockIsDesktopRunning), -})); - -let defaultProps: VegaConnectDialogProps; - -const INITIAL_KEY = 'some-key'; - -const jsonRpc = new JsonRpcConnector(); -const view = new ViewConnector(INITIAL_KEY); -const injected = new InjectedConnector(); -const connectors = { - jsonRpc, - view, - injected, - snap: undefined, -}; - -beforeEach(() => { - jest.clearAllMocks(); - defaultProps = { - connectors, - }; - - useVegaWalletDialogStore.setState({ - updateVegaWalletDialog: mockUpdateDialogOpen, - closeVegaWalletDialog: mockCloseVegaDialog, - vegaWalletDialogOpen: true, - }); -}); - -const defaultConfig: VegaWalletConfig = { - network: 'TESTNET', - vegaUrl: 'https://vega.xyz', - vegaWalletServiceUrl: 'https://vegaservice.xyz', - links: { - explorer: 'explorer-link', - concepts: 'concepts-link', - chromeExtensionUrl: 'chrome-link', - mozillaExtensionUrl: 'mozilla-link', - }, - chainId: 'VEGA_CHAIN_ID', -}; - -function generateJSX( - props?: Partial, - config?: Partial -) { - return ( - - - - ); -} - -describe('VegaConnectDialog', () => { - let navigatorGetter: jest.SpyInstance; - beforeEach(() => { - jest.clearAllMocks(); - navigatorGetter = jest.spyOn(window.navigator, 'userAgent', 'get'); - }); - it('displays a list of connection options', async () => { - const { container, rerender } = render(generateJSX()); - expect(container).toBeEmptyDOMElement(); - rerender(generateJSX()); - const list = await screen.findByTestId('connectors-list'); - expect(list).toBeInTheDocument(); - expect(list.children).toHaveLength(2); - expect(screen.getByTestId('connector-jsonRpc')).toHaveTextContent( - 'Use the Desktop App/CLI' - ); - }); - - it('displays browser wallet option if detected on window object', async () => { - navigatorGetter.mockReturnValue('Chrome'); - mockBrowserWallet(); - render(generateJSX()); - const list = await screen.findByTestId('connectors-list'); - expect(list.children).toHaveLength(2); - expect(screen.getByTestId('connector-injected')).toHaveTextContent( - 'Connect' - ); - - clearBrowserWallet(); - }); - - describe('JsonRpcConnector', () => { - const delay = 100; - let spyOnCheckCompat: jest.SpyInstance; - let spyOnGetChainId: jest.SpyInstance; - let spyOnConnectWallet: jest.SpyInstance; - let spyOnConnect: jest.SpyInstance; - - beforeEach(() => { - spyOnCheckCompat = jest - .spyOn(connectors.jsonRpc, 'checkCompat') - .mockImplementation(() => delayedResolve(true, delay)); - spyOnGetChainId = jest - .spyOn(connectors.jsonRpc, 'getChainId') - .mockImplementation(() => - delayedResolve({ chainID: mockChainId }, delay) - ); - spyOnConnectWallet = jest - .spyOn(connectors.jsonRpc, 'connectWallet') - .mockImplementation(() => delayedResolve(null, delay)); - spyOnConnect = jest - .spyOn(connectors.jsonRpc, 'connect') - .mockImplementation(() => - delayedResolve([{ publicKey: 'pubkey', name: 'test key 1' }], delay) - ); - }); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - localStorage.clear(); - }); - - it('connects with permission update', async () => { - render(generateJSX()); - await selectJsonRpc(); - - // Wallet version check - expect(screen.getByText('Checking wallet version')).toBeInTheDocument(); - expect(spyOnCheckCompat).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - - // Chain check - expect(screen.getByText('Verifying chain')).toBeInTheDocument(); - expect(spyOnGetChainId).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - - // Await user connect - expect(screen.getByText('Connecting...')).toBeInTheDocument(); - expect(spyOnConnectWallet).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - - // Connect (list keys) - expect(spyOnConnect).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - - expect(screen.getByText('Successfully connected')).toBeInTheDocument(); - - await act(async () => { - jest.advanceTimersByTime(CLOSE_DELAY); - }); - expect(mockCloseVegaDialog).toHaveBeenCalledWith(); - }); - - it('handles incompatible wallet', async () => { - spyOnCheckCompat - .mockClear() - .mockImplementation(() => delayedReject(ClientErrors.INVALID_WALLET)); - - render(generateJSX()); - await selectJsonRpc(); - - expect(screen.getByText('Checking wallet version')).toBeInTheDocument(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - expect(screen.getByText(/Wallet version invalid/i)).toBeInTheDocument(); - }); - - it('handles user rejection', async () => { - // 0002-WCON-002 - // 0002-WCON-005 - // 0002-WCON-007 - // 0002-WCON-015 - // 0003-WTXN-007 - spyOnConnectWallet - .mockClear() - .mockImplementation(() => - delayedReject( - new WalletError( - 'User error', - 3001, - 'The user rejected the request' - ), - delay - ) - ); - - render(generateJSX()); - await selectJsonRpc(); - - expect(spyOnCheckCompat).toHaveBeenCalled(); - expect(screen.getByText('Checking wallet version')).toBeInTheDocument(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - expect(spyOnGetChainId).toHaveBeenCalled(); - expect(screen.getByText('Verifying chain')).toBeInTheDocument(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - expect(spyOnConnectWallet).toHaveBeenCalled(); - expect(screen.getByText('Connecting...')).toBeInTheDocument(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - expect(screen.getByText('User error')).toBeInTheDocument(); - expect( - screen.getByText('The user rejected the request') - ).toBeInTheDocument(); - }); - - it('handles an unknown error', async () => { - // 0003-WTXN-009 - // 0003-WTXN-011 - // 0002-WCON-016 - // 0003-WTXN-008 - spyOnCheckCompat.mockClear().mockImplementation(() => { - throw new Error('unknown error'); - }); - - render(generateJSX()); - await selectJsonRpc(); - - expect(screen.getByText('Something went wrong')).toBeInTheDocument(); - expect(screen.getByText('An unknown error occurred')).toBeInTheDocument(); - }); - - it('handles wallet running is not detected', async () => { - mockIsDesktopRunning = false; - render(generateJSX()); - expect(await screen.findByTestId('connector-jsonRpc')).toBeDisabled(); - }); - - it('Mozilla logo should be rendered', async () => { - navigatorGetter.mockReturnValue('Firefox'); - render(generateJSX()); - expect(await screen.findByTestId('mozilla-logo')).toBeInTheDocument(); - }); - - it('Chrome logo should be rendered', async () => { - navigatorGetter.mockReturnValue('Chrome'); - render(generateJSX()); - expect(await screen.findByTestId('chrome-logo')).toBeInTheDocument(); - }); - - it('Chrome and Firefox logo should be rendered', async () => { - navigatorGetter.mockReturnValue('Safari'); - render(generateJSX()); - expect(await screen.findByTestId('mozilla-logo')).toBeInTheDocument(); - expect(await screen.findByTestId('chrome-logo')).toBeInTheDocument(); - }); - async function selectJsonRpc() { - expect(await screen.findByRole('dialog')).toBeInTheDocument(); - fireEvent.click(await screen.findByTestId('connector-jsonRpc')); - } - }); - - describe('InjectedConnector', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - localStorage.clear(); - }); - - afterEach(() => { - clearBrowserWallet(); - }); - - it('connects', async () => { - const delay = 100; - const vegaWindow = { - getChainId: jest.fn(() => - delayedResolve({ chainID: mockChainId }, delay) - ), - connectWallet: jest.fn(() => delayedResolve(null, delay)), - disconnectWallet: jest.fn(() => delayedResolve(undefined, delay)), - listKeys: jest.fn(() => - delayedResolve( - { - keys: [{ name: 'test key', publicKey: '0x123' }], - }, - 100 - ) - ), - }; - mockBrowserWallet(vegaWindow); - render(generateJSX()); - await selectInjected(); - - // Await user connect - expect(screen.getByText('Connecting...')).toBeInTheDocument(); - expect(vegaWindow.connectWallet).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - - // Connect (list keys) - expect(vegaWindow.listKeys).toHaveBeenCalled(); - await act(async () => { - jest.advanceTimersByTime(delay); - }); - expect(screen.getByText('Successfully connected')).toBeInTheDocument(); - - await act(async () => { - jest.advanceTimersByTime(CLOSE_DELAY); - }); - expect(mockCloseVegaDialog).toHaveBeenCalledWith(); - }); - - async function selectInjected() { - expect(await screen.findByRole('dialog')).toBeInTheDocument(); - fireEvent.click(await screen.findByTestId('connector-injected')); - } - }); -}); diff --git a/libs/wallet/src/connect-dialog/connect-dialog.tsx b/libs/wallet/src/connect-dialog/connect-dialog.tsx deleted file mode 100644 index 6892c4369..000000000 --- a/libs/wallet/src/connect-dialog/connect-dialog.tsx +++ /dev/null @@ -1,675 +0,0 @@ -import classNames from 'classnames'; -import { - Dialog, - ExternalLink, - Intent, - Pill, - TradingButton, - TradingFormGroup, - TradingInput, - VegaIcon, - VegaIconNames, -} from '@vegaprotocol/ui-toolkit'; -import { useCallback, useState, type ReactNode } from 'react'; -import { type WalletClientError } from '@vegaprotocol/wallet-client'; -import { type Connectors, type VegaConnector } from '../connectors'; -import { DEFAULT_SNAP_VERSION } from '../connectors'; -import { - DEFAULT_SNAP_ID, - InjectedConnector, - JsonRpcConnector, - SnapConnector, - ViewConnector, - requestSnap, -} from '../connectors'; -import { JsonRpcConnectorForm } from './json-rpc-connector-form'; -import { ViewConnectorForm } from './view-connector-form'; -import { - BrowserIcon, - ConnectDialogContent, - ConnectDialogFooter, - ConnectDialogTitle, -} from './connect-dialog-elements'; -import { - useJsonRpcConnect, - type Status as JsonRpcStatus, -} from '../use-json-rpc-connect'; -import { - useInjectedConnector, - type Status as InjectedStatus, -} from '../use-injected-connector'; -import { useVegaWallet } from '../use-vega-wallet'; -import { InjectedConnectorForm } from './injected-connector-form'; -import { isBrowserWalletInstalled } from '../utils'; -import { useIsWalletServiceRunning } from '../use-is-wallet-service-running'; -import { SnapStatus, useSnapStatus } from '../use-snap-status'; -import { useVegaWalletDialogStore } from './vega-wallet-dialog-store'; -import { useT } from '../use-t'; -import { Trans } from 'react-i18next'; - -export const CLOSE_DELAY = 1700; - -export type WalletType = 'injected' | 'jsonRpc' | 'view' | 'snap'; - -export interface VegaConnectDialogProps { - connectors: Connectors; - riskMessage?: ReactNode; - contentOnly?: boolean; - onClose?: () => void; -} - -export const VegaConnectDialog = ({ - connectors, - riskMessage, - contentOnly, - onClose, -}: VegaConnectDialogProps) => { - const { chainId, disconnect, acknowledgeNeeded } = useVegaWallet(); - const vegaWalletDialogOpen = useVegaWalletDialogStore( - (store) => store.vegaWalletDialogOpen - ); - const updateVegaWalletDialog = useVegaWalletDialogStore( - (store) => (open: boolean) => { - store.updateVegaWalletDialog(open); - } - ); - - const onVegaWalletDialogChange = useCallback( - (open: boolean) => { - updateVegaWalletDialog(open); - if (!open && acknowledgeNeeded) { - disconnect(); - } - }, - [updateVegaWalletDialog, acknowledgeNeeded, disconnect] - ); - - const content = chainId && ( - - ); - if (contentOnly) { - return content; - } - return ( - - {content} - - ); -}; - -const ConnectDialogContainer = ({ - connectors, - appChainId, - riskMessage, - onClose, -}: { - connectors: Connectors; - appChainId: string; - riskMessage?: ReactNode; - onClose?: () => void; -}) => { - const { vegaUrl, vegaWalletServiceUrl } = useVegaWallet(); - const closeVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.closeVegaWalletDialog - ); - const closeDialog = useCallback(() => { - onClose ? onClose() : closeVegaWalletDialog(); - }, [closeVegaWalletDialog, onClose]); - const [selectedConnector, setSelectedConnector] = useState(); - const [walletUrl, setWalletUrl] = useState(vegaWalletServiceUrl); - - const reset = useCallback(() => { - setSelectedConnector(undefined); - }, []); - - const delayedOnConnect = useCallback(() => { - setTimeout(() => { - closeDialog(); - }, CLOSE_DELAY); - }, [closeDialog]); - - const { connect: jsonRpcConnect, ...jsonRpcState } = - useJsonRpcConnect(delayedOnConnect); - const { connect: injectedConnect, ...injectedState } = - useInjectedConnector(delayedOnConnect); - - const handleSelect = (type: WalletType) => { - const connector = connectors[type]; - - if (!connector) { - // we should never get here unless connectors are not configured correctly - throw new Error(`Connector type: ${type} not configured`); - } - - setSelectedConnector(connector); - - // Immediately connect on selection if jsonRpc is selected, we can't do this - // for rest because we need to show an authentication form - if (connector instanceof JsonRpcConnector) { - connector.url = walletUrl; - jsonRpcConnect(connector, appChainId); - } else if (connector instanceof InjectedConnector) { - injectedConnect(connector, appChainId); - } else if (connector instanceof SnapConnector) { - // Set the nodeAddress to send tx's to, normally this is handled by - // the vega wallet - connector.nodeAddress = new URL(vegaUrl).origin; - injectedConnect(connector, appChainId); - } - }; - - const isDesktopWalletRunning = useIsWalletServiceRunning( - walletUrl, - connectors['jsonRpc'] - ); - - const snapStatus = useSnapStatus( - DEFAULT_SNAP_ID, - Boolean(connectors['snap']) - ); - - return ( - <> - - {selectedConnector !== undefined ? ( - - ) : ( - - )} - - - - ); -}; - -const ConnectorList = ({ - connectors, - onSelect, - walletUrl, - setWalletUrl, - isDesktopWalletRunning, - snapStatus, -}: { - connectors: Connectors; - onSelect: (type: WalletType) => void; - walletUrl: string; - setWalletUrl: (value: string) => void; - isDesktopWalletRunning: boolean | null; - snapStatus: SnapStatus; -}) => { - const t = useT(); - const { pubKey, links } = useVegaWallet(); - const title = isBrowserWalletInstalled() - ? t('Connect Vega wallet') - : t('Get a Vega wallet'); - - const extendedText = ( - <> -
    - {t('Connect')} -
    - - - ); - const isItChrome = window.navigator.userAgent.includes('Chrome'); - const isItMozilla = - window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - const browserName = isItChrome - ? 'Chrome' - : isItMozilla - ? 'Firefox' - : t('your browser'); - - return ( - <> - {title} -

    - {t( - 'Connect securely, deposit funds and approve or reject transactions with the Vega wallet' - )} -

    -
    - {isBrowserWalletInstalled() ? ( - onSelect('injected')} - title={ - full featured]} - /> - } - description={t( - 'Connect with Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.', - { browserName } - )} - /> - ) : ( -
    -

    - full featured]} - /> -

    -

    - {t( - 'Install Vega Wallet extension for {{browserName}} to access all features including key management and detailed transaction views from your browser.', - { browserName } - )} -

    - -
    - )} - {connectors['snap'] !== undefined ? ( -
    - {snapStatus === SnapStatus.INSTALLED ? ( - quick start]} - /> - } - description={t( - `Connect directly via Metamask with the Vega Snap for single key support without advanced features.` - )} - text={ - <> -
    - {t('Connect via Vega MetaMask Snap')} -
    -
    - -
    - - } - onClick={() => { - onSelect('snap'); - }} - /> - ) : ( - <> - quick start, - ]} - /> - } - description={t( - `Install Metamask with the Vega Snap for single key support without advanced features.` - )} - text={ - <> -
    - {t('Install Vega MetaMask Snap')} -
    -
    - -
    - - } - onClick={() => { - requestSnap(DEFAULT_SNAP_ID, { - version: DEFAULT_SNAP_VERSION, - }); - }} - /> - {snapStatus === SnapStatus.NOT_SUPPORTED ? ( -

    - - MetaMask Snaps - , - ]} - /> -

    - ) : null} - - )} -
    - ) : null} -
    -

    {t('Advanced / Other options...')}

    - onSelect('view')} - disabled={Boolean(pubKey)} - /> -
    - onSelect('jsonRpc')} - /> -
    -
    -
    - - ); -}; - -const SelectedForm = ({ - connector, - appChainId, - jsonRpcState, - injectedState, - reset, - onConnect, - riskMessage, -}: { - connector: VegaConnector; - appChainId: string; - jsonRpcState: { - status: JsonRpcStatus; - error: WalletClientError | null; - }; - injectedState: { - status: InjectedStatus; - error: Error | null; - }; - reset: () => void; - onConnect: () => void; - riskMessage?: ReactNode; -}) => { - if ( - connector instanceof InjectedConnector || - connector instanceof SnapConnector - ) { - return ( - - ); - } - - if (connector instanceof JsonRpcConnector) { - return ( - - ); - } - if (connector instanceof ViewConnector) { - return ( - - ); - } - throw new Error('No connector selected'); -}; - -export const GetWalletButton = ({ - chromeExtensionUrl, - mozillaExtensionUrl, - className, -}: { - chromeExtensionUrl?: string; - mozillaExtensionUrl?: string; - className?: string; -}) => { - const t = useT(); - const isItChrome = window.navigator.userAgent.includes('Chrome'); - const isItMozilla = - window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - - const onClick = () => { - if (isItMozilla) { - window.open(mozillaExtensionUrl, '_blank'); - return; - } - if (isItChrome) { - window.open(chromeExtensionUrl, '_blank'); - } - }; - - const buttonContent = ( - <> -
    - {t('Get the Vega Wallet')} - - ALPHA - -
    - {chromeExtensionUrl && mozillaExtensionUrl && ( - - )} - - ); - - return !isItChrome && !isItMozilla ? ( -
    - {buttonContent} -
    - ) : ( - - {buttonContent} - - ); -}; - -const ConnectionOptionWithDescription = ({ - disabled, - type, - text, - onClick, - icon, - description, - title, -}: { - type: WalletType; - text: string | ReactNode; - onClick: () => void; - disabled?: boolean; - icon?: ReactNode; - description?: string | ReactNode; - title?: string | ReactNode; -}) => { - return ( -
    -

    {title}

    -

    {description}

    - -
    - ); -}; - -const ConnectionOption = ({ - disabled, - type, - text, - onClick, - icon, -}: { - type: WalletType; - text: string | ReactNode; - onClick: () => void; - disabled?: boolean; - icon?: ReactNode; -}) => { - return ( - - {text} - - ); -}; - -const CustomUrlInput = ({ - walletUrl, - setWalletUrl, - isDesktopWalletRunning, - onSelect, -}: { - walletUrl: string; - setWalletUrl: (url: string) => void; - isDesktopWalletRunning: boolean | null; - onSelect: (type: WalletType) => void; -}) => { - const t = useT(); - const { pubKey } = useVegaWallet(); - const [urlInputExpanded, setUrlInputExpanded] = useState(false); - return urlInputExpanded ? ( - <> -
    -

    {t('Custom wallet location')}

    - -
    - - setWalletUrl(e.target.value)} - name="wallet-url" - /> - - onSelect('jsonRpc')} - /> - - ) : ( - <> - onSelect('jsonRpc')} - /> - {isDesktopWalletRunning !== null && ( -
    - {isDesktopWalletRunning ? ( - - ) : ( -

    - - No running Desktop App/CLI detected. Open your app now to - connect or enter a - , - , - ]} - /> -

    - )} -
    - )} - - ); -}; diff --git a/libs/wallet/src/connect-dialog/index.ts b/libs/wallet/src/connect-dialog/index.ts deleted file mode 100644 index ee9771191..000000000 --- a/libs/wallet/src/connect-dialog/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './connect-dialog'; -export * from './view-as-dialog'; -export * from './vega-wallet-dialog-store'; diff --git a/libs/wallet/src/connect-dialog/injected-connector-form.tsx b/libs/wallet/src/connect-dialog/injected-connector-form.tsx deleted file mode 100644 index e5e3bd2e8..000000000 --- a/libs/wallet/src/connect-dialog/injected-connector-form.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { Status } from '../use-injected-connector'; -import { ConnectDialogTitle } from './connect-dialog-elements'; -import type { ReactNode } from 'react'; -import { - Button, - ButtonLink, - Diamond, - Loader, - Tick, -} from '@vegaprotocol/ui-toolkit'; -import { setAcknowledged } from '../storage'; -import { useVegaWallet } from '../use-vega-wallet'; -import { InjectedConnectorErrors, SnapConnectorErrors } from '../connectors'; -import { useT } from '../use-t'; - -export const InjectedConnectorForm = ({ - status, - onConnect, - riskMessage, - appChainId, - reset, - error, -}: { - appChainId: string; - status: Status; - error: Error | null; - onConnect: () => void; - reset: () => void; - riskMessage?: React.ReactNode; -}) => { - const t = useT(); - const { disconnect } = useVegaWallet(); - - if (status === Status.Idle) { - return null; - } - - if (status === Status.Error) { - return ; - } - - if (status === Status.GettingChainId) { - return ( - <> - {t('Verifying chain')} -
    - -
    - - ); - } - - if (status === Status.Connected) { - return ( - <> - {t('Successfully connected')} -
    - -
    - - ); - } - - if (status === Status.Connecting) { - return ( - <> - {t('Connecting...')} -
    - -
    -

    - {t( - "Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with." - )} -

    - - ); - } - - if (status === Status.AcknowledgeNeeded) { - const setConnection = () => { - setAcknowledged(); - onConnect(); - }; - const handleDisagree = () => { - disconnect(); - onConnect(); // this is dialog closing - }; - return ( - <> - {t('Understand the risk')} - {riskMessage} -
    -
    - -
    -
    - -
    -
    - - ); - } - return null; -}; - -const Center = ({ children }: { children: ReactNode }) => { - return ( -
    {children}
    - ); -}; - -const Error = ({ - error, - appChainId, - onTryAgain, -}: { - error: Error | null; - appChainId: string; - onTryAgain: () => void; -}) => { - const t = useT(); - let title = t('Something went wrong'); - let text: ReactNode | undefined = t('An unknown error occurred'); - const tryAgain: ReactNode | null = ( -

    - {t('Try again')} -

    - ); - - if (error) { - if (error.message === InjectedConnectorErrors.USER_REJECTED.message) { - title = t('User rejected'); - text = t('The user rejected the wallet connection'); - } else if ( - error.message === InjectedConnectorErrors.INVALID_CHAIN.message - ) { - title = t('Wrong network'); - text = t( - 'To complete your wallet connection, set your wallet network in your app to "{{appChainId}}".', - { appChainId } - ); - } else if ( - error.message === InjectedConnectorErrors.VEGA_UNDEFINED.message - ) { - title = t('No wallet detected'); - text = t('Vega browser extension not installed'); - } else if ( - error.message === SnapConnectorErrors.ETHEREUM_UNDEFINED.message || - error.message === SnapConnectorErrors.NODE_ADDRESS_NOT_SET.message - ) { - title = t('Snap failed'); - text = t('Could not connect to Vega MetaMask Snap'); - } - } - - return ( - <> - {title} -

    {text}

    - {tryAgain} - - ); -}; diff --git a/libs/wallet/src/connect-dialog/json-rpc-connector-form.tsx b/libs/wallet/src/connect-dialog/json-rpc-connector-form.tsx deleted file mode 100644 index f8bac0a89..000000000 --- a/libs/wallet/src/connect-dialog/json-rpc-connector-form.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import capitalize from 'lodash/capitalize'; -import { - Button, - ButtonLink, - Diamond, - Link, - Loader, - Tick, -} from '@vegaprotocol/ui-toolkit'; -import type { ReactNode } from 'react'; -import type { WalletClientError } from '@vegaprotocol/wallet-client'; -import type { JsonRpcConnector } from '../connectors'; -import { ClientErrors } from '../connectors'; -import { ConnectDialogTitle } from './connect-dialog-elements'; -import { Status } from '../use-json-rpc-connect'; -import { useVegaWallet } from '../use-vega-wallet'; -import { setAcknowledged } from '../storage'; -import { useT } from '../use-t'; - -export const ServiceErrors = { - NO_HEALTHY_NODE: 1000, - REQUEST_PROCESSING: -32000, -}; - -export const JsonRpcConnectorForm = ({ - connector, - appChainId, - status, - error, - reset, - onConnect, - riskMessage, -}: { - connector: JsonRpcConnector; - appChainId: string; - status: Status; - error: WalletClientError | null; - onConnect: () => void; - reset: () => void; - riskMessage?: React.ReactNode; -}) => { - const t = useT(); - const { disconnect } = useVegaWallet(); - if (status === Status.Idle) { - return null; - } - - if (status === Status.Error) { - return ( - - ); - } - - if (status === Status.CheckingVersion) { - return ( - <> - {t('Checking wallet version')} -
    - -
    -

    - {t('Checking your wallet is compatible with this app')} -

    - - ); - } - - if (status === Status.GettingChainId) { - return ( - <> - {t('Verifying chain')} -
    - -
    - - ); - } - - if (status === Status.Connected) { - return ( - <> - {t('Successfully connected')} -
    - -
    - - ); - } - - if (status === Status.Connecting || status === Status.GettingPerms) { - return ( - <> - {t('Connecting...')} -
    - -
    -

    - {t( - "Approve the connection from your Vega wallet app. If you have multiple wallets you'll need to choose which to connect with." - )} -

    - - ); - } - - if (status === Status.AcknowledgeNeeded) { - const setConnection = () => { - setAcknowledged(); - onConnect(); - }; - const handleDisagree = () => { - disconnect(); - onConnect(); // this is dialog closing - }; - return ( - <> - {t('Understand the risk')} - {riskMessage} -
    -
    - -
    -
    - -
    -
    - - ); - } - return null; -}; - -const Center = ({ children }: { children: ReactNode }) => { - return ( -
    {children}
    - ); -}; - -const Error = ({ - error, - connectorUrl, - appChainId, - onTryAgain, -}: { - error: WalletClientError | null; - connectorUrl: string | null; - appChainId: string; - onTryAgain: () => void; -}) => { - const t = useT(); - const { links } = useVegaWallet(); - let title = t('Something went wrong'); - let text: ReactNode | undefined = t('An unknown error occurred'); - let tryAgain: ReactNode | null = ( -

    - {t('Try again')} -

    - ); - - if (error) { - if (error.code === ClientErrors.NO_SERVICE.code) { - title = t('No wallet detected'); - text = connectorUrl - ? t('No wallet application running at {{connectorUrl}}', { - connectorUrl, - }) - : t('No Vega Wallet application running'); - } else if (error.code === ClientErrors.WRONG_NETWORK.code) { - title = t('Wrong network'); - text = t( - 'To complete your wallet connection, set your wallet network in your app to "{{appChainId}}".', - { appChainId } - ); - } else if (error.code === ServiceErrors.NO_HEALTHY_NODE) { - title = error.title; - text = ( - <> - {capitalize(error.message)} - {'. '} - - {t('Read the docs to troubleshoot')} - - - ); - } else if (error.code === ServiceErrors.REQUEST_PROCESSING) { - title = t('Connection in progress'); - text = t('Approve the connection from your Vega wallet app.'); - tryAgain = null; - } else if (error.code === 0) { - title = t('Wrong network'); - text = ( - <> - {t( - 'To complete your wallet connection, set your wallet network in your app to "{{appChainId}}".', - { appChainId } - )} - - ); - } else if (error.code === ClientErrors.INVALID_WALLET.code) { - title = error.title; - const errorData = error.message?.split('\n ') || []; - text = ( - - {errorData.map((str, i) => ( - {str} - ))} - - ); - } else { - title = error.title; - text = error.message; - } - } - - return ( - <> - {title} -

    {text}

    - {tryAgain} - - ); -}; diff --git a/libs/wallet/src/connect-dialog/mozilla-icon.tsx b/libs/wallet/src/connect-dialog/mozilla-icon.tsx deleted file mode 100644 index 8197e2cbd..000000000 --- a/libs/wallet/src/connect-dialog/mozilla-icon.tsx +++ /dev/null @@ -1,214 +0,0 @@ -export const MozillaIcon = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/libs/wallet/src/connect-dialog/vega-wallet-dialog-store.ts b/libs/wallet/src/connect-dialog/vega-wallet-dialog-store.ts deleted file mode 100644 index 824be1c1c..000000000 --- a/libs/wallet/src/connect-dialog/vega-wallet-dialog-store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { create } from 'zustand'; - -export interface VegaWalletDialogStore { - vegaWalletDialogOpen: boolean; - updateVegaWalletDialog: (open: boolean) => void; - openVegaWalletDialog: () => void; - closeVegaWalletDialog: () => void; -} - -export const useVegaWalletDialogStore = create()( - (set) => ({ - vegaWalletDialogOpen: false, - updateVegaWalletDialog: (open: boolean) => - set({ vegaWalletDialogOpen: open }), - openVegaWalletDialog: () => set({ vegaWalletDialogOpen: true }), - closeVegaWalletDialog: () => set({ vegaWalletDialogOpen: false }), - }) -); diff --git a/libs/wallet/src/connect-dialog/view-as-dialog.tsx b/libs/wallet/src/connect-dialog/view-as-dialog.tsx deleted file mode 100644 index 4bca8d9dd..000000000 --- a/libs/wallet/src/connect-dialog/view-as-dialog.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Dialog } from '@vegaprotocol/ui-toolkit'; -import { ViewConnectorForm } from './view-connector-form'; -import type { ViewConnector } from '../connectors'; -import { create } from 'zustand'; - -type ViewAsDialogStore = { - open: boolean; - setOpen: (open: boolean) => void; -}; - -export const useViewAsDialog = create()((set) => ({ - open: false, - setOpen: (open) => set({ open }), -})); - -type ViewAsDialogProps = { - connector: ViewConnector; -}; - -export const ViewAsDialog = ({ connector }: ViewAsDialogProps) => { - const open = useViewAsDialog((state) => state.open); - const setOpen = useViewAsDialog((state) => state.setOpen); - - return ( - { - setOpen(open); - }} - > - { - setOpen(false); - }} - /> - - ); -}; diff --git a/libs/wallet/src/connect-dialog/view-connector-form.tsx b/libs/wallet/src/connect-dialog/view-connector-form.tsx deleted file mode 100644 index d70e032e0..000000000 --- a/libs/wallet/src/connect-dialog/view-connector-form.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { - TradingFormGroup, - TradingInput, - TradingInputError, - Intent, - TradingButton, - VegaIcon, - VegaIconNames, -} from '@vegaprotocol/ui-toolkit'; -import { useForm } from 'react-hook-form'; -import type { ViewConnector } from '../connectors'; -import { useVegaWallet } from '../use-vega-wallet'; -import { ConnectDialogTitle } from './connect-dialog-elements'; -import { useT } from '../use-t'; - -interface FormFields { - address: string; -} - -interface ViewConnectorFormProps { - connector: ViewConnector; - onConnect: (connector: ViewConnector) => void; - reset?: () => void; -} - -export function ViewConnectorForm({ - connector, - onConnect, - reset, -}: ViewConnectorFormProps) { - const t = useT(); - const { connect } = useVegaWallet(); - const { - register, - handleSubmit, - formState: { errors }, - } = useForm(); - - const validatePubkey = (value: string) => { - const number = +`0x${value}`; - if (value.length !== 64) { - return t('Pubkey must be 64 characters in length'); - } else if (Number.isNaN(number)) { - return t('Pubkey must be be valid hex'); - } - return true; - }; - - async function onSubmit(fields: FormFields) { - await connector.setPubkey(fields.address); - await connect(connector); - onConnect(connector); - } - - return ( - <> - {t('VIEW AS VEGA USER')} -
    -

    - {t( - 'Browse from the perspective of another Vega user in read-only mode.' - )} -

    - - - {errors.address?.message && ( - - {errors.address.message} - - )} - - - {t('Browse network')} - - {reset && ( -
    - -
    - )} -
    - - ); -} diff --git a/libs/wallet/src/connectors/index.ts b/libs/wallet/src/connectors/index.ts index 7ab8c4d74..acef6724b 100644 --- a/libs/wallet/src/connectors/index.ts +++ b/libs/wallet/src/connectors/index.ts @@ -1,17 +1,5 @@ -import type { InjectedConnector } from './injected-connector'; -import type { JsonRpcConnector } from './json-rpc-connector'; -import type { SnapConnector } from './snap-connector'; -import type { ViewConnector } from './view-connector'; - -export * from './injected-connector'; -export * from './json-rpc-connector'; -export * from './snap-connector'; -export * from './vega-connector'; -export * from './view-connector'; - -export type Connectors = { - jsonRpc: JsonRpcConnector | undefined; - injected: InjectedConnector | undefined; - snap: SnapConnector | undefined; - view: ViewConnector | undefined; -}; +export { InjectedConnector } from './injected-connector'; +export { SnapConnector } from './snap-connector'; +export { JsonRpcConnector } from './json-rpc-connector'; +export { ViewPartyConnector } from './view-party-connector'; +export { MockConnector, mockKeys } from './mock-connector'; diff --git a/libs/wallet/src/connectors/injected-connector.ts b/libs/wallet/src/connectors/injected-connector.ts index e238eac74..9d8bbfc3a 100644 --- a/libs/wallet/src/connectors/injected-connector.ts +++ b/libs/wallet/src/connectors/injected-connector.ts @@ -1,143 +1,99 @@ -import { clearConfig, setConfig } from '../storage'; -import type { Transaction, VegaConnector } from './vega-connector'; +import { + ConnectorError, + chainIdError, + connectError, + disconnectError, + isConnectedError, + listKeysError, + noWalletError, + sendTransactionError, +} from '../errors'; +import { + type TransactionParams, + type Connector, + type VegaWalletEvent, +} from '../types'; -type VegaWalletEvent = 'client.disconnected'; +export class InjectedConnector implements Connector { + readonly id = 'injected'; + readonly name = 'Vega Wallet'; + readonly description = + 'Connect with Vega Wallet extension to access all features including key management and detailed transaction views from your browser.'; -declare global { - interface Vega { - connectWallet: (args: { chainId: string }) => Promise; - disconnectWallet: () => Promise; - listKeys: () => Promise<{ - keys: Array<{ name: string; publicKey: string }>; - }>; - sendTransaction: (params: { - publicKey: string; - transaction: Transaction; - sendingMode: 'TYPE_SYNC'; - }) => Promise<{ - receivedAt: string; - sentAt: string; - transaction: { - from: { - pubKey: string; - }; - inputData: string; - pow: { - tid: string; - nonce: string; - }; - signature: { - algo: string; - value: string; - version: number; - }; - version: number; - }; - transactionHash: string; - }>; - - on: (event: VegaWalletEvent, callback: () => void) => void; - isConnected?: () => Promise; - } - - interface Window { - vega: Vega; - } -} - -export const InjectedConnectorErrors = { - USER_REJECTED: new Error('Connection denied'), - VEGA_UNDEFINED: new Error('window.vega not found'), - INVALID_CHAIN: new Error('Invalid chain'), -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const wait = (ms: number) => - new Promise((_, reject) => { - setTimeout(() => { - reject(false); - }, ms); - }); - -const INJECTED_CONNECTOR_TIMEOUT = 1000; - -export class InjectedConnector implements VegaConnector { - isConnected = false; - chainId: string | null = null; - description = 'Connects using the Vega wallet browser extension'; - alive: ReturnType | undefined = undefined; + bindStore() {} async connectWallet(chainId: string) { - this.chainId = chainId; try { + if (window.vega === undefined) { + throw noWalletError('window.vega is undefined'); + } + await window.vega.connectWallet({ chainId }); - this.isConnected = true; - window.vega.on('client.disconnected', () => { - this.isConnected = false; - }); - - this.alive = setInterval(async () => { - try { - const connected = await Promise.race([ - // FIXME: All of the `window.vega` initiated promises are `pending` - // while waiting for the user action when transaction is sent. - // (Probably due to the FIFO queue of the `PortServer`?) - // Because of that we cannot `wait` here as while waiting for the - // user action in wallet this will `reject`. It'd be cool if the - // `window.vega` was not blocking the api calls. - - // wait(INJECTED_CONNECTOR_TIMEOUT), - - // `isConnected` is only available in the newer versions - // of the browser wallet - 'isConnected' in window.vega && - typeof window.vega.isConnected === 'function' - ? window.vega.isConnected() - : window.vega.listKeys(), - ]); - this.isConnected = Boolean(connected); - } catch { - this.isConnected = false; - } - }, INJECTED_CONNECTOR_TIMEOUT * 2); - } catch { - throw new Error( - `could not connect to the vega wallet on chain: ${chainId}` - ); + return { success: true }; + } catch (err) { + if (err instanceof ConnectorError) { + throw err; + } + throw connectError(); } } - async connect() { - const res = await window.vega.listKeys(); - setConfig({ - connector: 'injected', - token: null, // no token required for injected - url: null, // no url for injected - }); - return res.keys; + async disconnectWallet() { + try { + await window.vega.disconnectWallet(); + } catch (err) { + throw disconnectError(); + } } - async isAlive() { - return this.isConnected; + // deprecated, pass chain on connect + async getChainId() { + try { + const res = await window.vega.getChainId(); + return { chainId: res.chainID }; + } catch (err) { + throw chainIdError(); + } } - disconnect() { - clearInterval(this.alive); - clearConfig(); - return window.vega.disconnectWallet(); + async listKeys() { + try { + const res = await window.vega.listKeys(); + return res.keys; + } catch (err) { + throw listKeysError(); + } } - async sendTx(pubKey: string, transaction: Transaction) { - const result = await window.vega.sendTransaction({ - publicKey: pubKey, - transaction, - sendingMode: 'TYPE_SYNC' as const, - }); - return { - transactionHash: result.transactionHash, - receivedAt: result.receivedAt, - sentAt: result.sentAt, - signature: result.transaction.signature.value, - }; + async isConnected() { + try { + const res = await window.vega.isConnected(); + return { connected: res }; + } catch (err) { + throw isConnectedError(); + } + } + + async sendTransaction(params: TransactionParams) { + try { + const res = await window.vega.sendTransaction(params); + + return { + transactionHash: res.transactionHash, + signature: res.transaction.signature.value, + receivedAt: res.receivedAt, + sentAt: res.sentAt, + }; + } catch (err) { + throw sendTransactionError(); + } + } + + on(event: VegaWalletEvent, callback: () => void) { + window.vega.on(event, callback); + } + + off(event: VegaWalletEvent, callback: () => void) { + window.vega.off(event, callback); } } diff --git a/libs/wallet/src/connectors/json-rpc-connector.ts b/libs/wallet/src/connectors/json-rpc-connector.ts index 63db02041..3faa1c953 100644 --- a/libs/wallet/src/connectors/json-rpc-connector.ts +++ b/libs/wallet/src/connectors/json-rpc-connector.ts @@ -1,192 +1,230 @@ -import { WalletClient, WalletClientError } from '@vegaprotocol/wallet-client'; -import { clearConfig, getConfig, setConfig } from '../storage'; -import type { Transaction, VegaConnector } from './vega-connector'; -import { WalletError } from './vega-connector'; +import { type StoreApi } from 'zustand'; +import { EventEmitter } from 'eventemitter3'; +import { + JsonRpcMethod, + type Connector, + type TransactionParams, + type Store, + type VegaWalletEvent, +} from '../types'; +import { + ConnectorError, + connectError, + noWalletError, + sendTransactionError, + userRejectedError, +} from '../errors'; -const VERSION = 'v2'; +type JsonRpcConnectorConfig = { url: string; token?: string }; -export const ClientErrors = { - NO_SERVICE: new WalletError('No service', 100), - INVALID_WALLET: new WalletError('Wallet version invalid', 103), - WRONG_NETWORK: new WalletError( - 'Wrong network', - 104, - 'App is configured to work with a different chain' - ), - UNKNOWN: new WalletError( - 'Something went wrong', - 105, - 'Unknown error occurred' - ), - NO_CLIENT: new WalletError('No client found.', 106), -} as const; +export class JsonRpcConnector implements Connector { + readonly id = 'jsonRpc'; + readonly name = 'Command Line Wallet'; + readonly description = + 'Connect using the command line wallet or the legacy desktop app.'; -export class JsonRpcConnector implements VegaConnector { - version = VERSION; - private _url: string | null = null; - token: string | null = null; - reqId = 0; - client?: WalletClient; + url: string; + requestId: number = 0; + store: StoreApi | undefined; + pollRef: NodeJS.Timer | undefined; + ee: EventEmitter; - constructor() { - const cfg = getConfig(); - - this.token = cfg?.token ?? null; - - if (cfg && cfg.url) { - this.url = cfg.url; - this.client = new WalletClient({ - address: cfg.url, - token: cfg.token ?? undefined, - onTokenChange: (token) => { - this.token = token; - setConfig({ - token, - connector: 'jsonRpc', - url: this._url, - }); - }, - }); - } + constructor(config: JsonRpcConnectorConfig) { + this.url = config.url; + this.ee = new EventEmitter(); } - set url(url: string) { - this._url = url; - this.client = new WalletClient({ - address: url, - token: this.token ?? undefined, - onTokenChange: (token) => { - this.token = token; - setConfig({ - token, - url, - connector: 'jsonRpc', - }); - }, - }); + bindStore(store: StoreApi) { + this.store = store; } - get url() { - return this._url || ''; - } - async getChainId() { - if (!this.client) { - throw ClientErrors.NO_CLIENT; - } + + async connectWallet(desiredChainId: string) { try { - const { result } = await this.client.GetChainId(); - return result; - } catch (err) { - const { - code = ClientErrors.UNKNOWN.code, - message = ClientErrors.UNKNOWN.message, - title, - } = err as WalletClientError; - throw new WalletError(title, code, message); - } - } + const chainRes = await this.getChainId(); - async connectWallet() { - if (!this.client) { - throw ClientErrors.NO_CLIENT; - } + if ('error' in chainRes) { + throw connectError('getChainId failed'); + } - try { - await this.client.ConnectWallet(); - return null; - } catch (err) { - const { - code = ClientErrors.UNKNOWN.code, - message = ClientErrors.UNKNOWN.message, - title, - } = err as WalletClientError; - throw new WalletError(title, code, message); - } - } + if (chainRes.chainId !== desiredChainId) { + throw connectError( + `desired chain is ${desiredChainId} but wallet chain is ${chainRes.chainId}` + ); + } - // connect actually calling list_keys here, not to be confused with connect_wallet - // which retrieves the session token - async connect() { - if (!this.client) { - throw ClientErrors.NO_CLIENT; - } + if (!this.token) { + const { response, data } = await this.request( + JsonRpcMethod.ConnectWallet, + { + hostname: window.location.hostname, + } + ); - try { - const { result } = await this.client.ListKeys(); - return result.keys; - } catch (err) { - const { - code = ClientErrors.UNKNOWN.code, - message = ClientErrors.UNKNOWN.message, - title, - } = err as WalletClientError; - throw new WalletError(title, code, message); - } - } + const token = response.headers.get('Authorization'); - async isAlive() { - if (this.client) { - try { - const keys = await this.client.ListKeys(); - if (keys.result.keys.length > 0) { - return true; + if (!response.ok) { + if ('error' in data && data.error.code === 3001) { + throw userRejectedError(); + } + throw connectError('response not ok'); } - } catch (err) { - return false; + + if (!token) { + throw connectError('no Authorization header'); + } + + this.token = token; } - } - return false; - } - - async disconnect() { - if (!this.client) { - throw ClientErrors.NO_CLIENT; - } - - try { - await this.client.DisconnectWallet(); + this.startPoll(); + return { success: true }; } catch (err) { - // NOOP - } - clearConfig(); - } - - async sendTx(pubKey: string, transaction: Transaction) { - if (!this.client) { - throw ClientErrors.NO_CLIENT; - } - - const { result } = await this.client.SendTransaction({ - publicKey: pubKey, - sendingMode: 'TYPE_SYNC', - transaction, - }); - - return { - transactionHash: result.transactionHash, - sentAt: result.sentAt, - receivedAt: result.receivedAt, - signature: result.transaction.signature.value, - }; - } - - async checkCompat() { - try { - const result = await fetch(`${this._url}/api/${this.version}/methods`); - if (!result.ok) { - const sent1 = `The version of the wallet service running at ${this._url} is not supported.`; - const sent2 = `Update the wallet software to a version that expose the API ${this.version}.`; - const data = `${sent1}\n ${sent2}`; - const title = 'Wallet version invalid'; - throw new WalletError(title, ClientErrors.INVALID_WALLET.code, data); - } - return true; - } catch (err) { - if (err instanceof WalletClientError) { + if (err instanceof ConnectorError) { throw err; } - throw ClientErrors.NO_SERVICE; + throw noWalletError(); } } + + async disconnectWallet() { + try { + this.stopPoll(); + await this.request(JsonRpcMethod.DisconnectWallet); + } catch (err) { + throw noWalletError(); + } + } + + // deprecated, pass chain on connect + async getChainId() { + try { + const { data } = await this.request(JsonRpcMethod.GetChainId); + + return { chainId: data.result.chainID }; + } catch (err) { + this.stopPoll(); + throw noWalletError(); + } + } + + async listKeys() { + try { + const { data } = await this.request(JsonRpcMethod.ListKeys); + return data.result.keys as Array<{ publicKey: string; name: string }>; + } catch (err) { + this.stopPoll(); + throw noWalletError(); + } + } + + async isConnected() { + try { + await this.listKeys(); + return { connected: true }; + } catch { + this.stopPoll(); + return { connected: false }; + } + } + + async sendTransaction(params: TransactionParams) { + try { + const { response, data } = await this.request( + JsonRpcMethod.SendTransaction, + params + ); + + if (!response.ok) { + if ('error' in data) { + if (data.error.code === 3001) { + throw userRejectedError(); + } + + throw sendTransactionError( + `${data.error.message}: ${data.error.data}` + ); + } + + throw sendTransactionError('response not ok'); + } + + return { + transactionHash: data.result.transactionHash, + signature: data.result.transaction.signature.value, + receivedAt: data.result.receivedAt, + sentAt: data.result.sentAt, + }; + } catch (err) { + if (err instanceof ConnectorError) { + throw err; + } + + throw noWalletError(); + } + } + + on(event: VegaWalletEvent, callback: () => void) { + this.ee.on(event, callback); + } + + off(event: VegaWalletEvent, callback?: () => void) { + this.ee.off(event, callback); + } + + //////////////////////////////////// + // JSON rpc connector 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); + } + } + + // TODO: fix any + // eslint-disable-next-line + private async request(method: JsonRpcMethod, params?: any) { + const headers = new Headers(); + + if (this.token) { + headers.set('Authorization', this.token); + } + + const response = await fetch(`${this.url}/api/v2/requests`, { + method: 'POST', + headers, + body: JSON.stringify({ + id: `${this.requestId++}`, + jsonrpc: '2.0', + method, + params, + }), + }); + + const data = await response.json(); + + return { + data, + response, + }; + } + + get token() { + return this.store?.getState().jsonRpcToken; + } + + set token(value: string | undefined) { + this.store?.setState({ jsonRpcToken: value }); + } } diff --git a/libs/wallet/src/connectors/mock-connector.ts b/libs/wallet/src/connectors/mock-connector.ts new file mode 100644 index 000000000..07e986223 --- /dev/null +++ b/libs/wallet/src/connectors/mock-connector.ts @@ -0,0 +1,57 @@ +import { mockChain } from '../chains'; +import { type Connector } from '../types'; + +export const mockKeys = [ + { + name: 'Key 1', + publicKey: '1'.repeat(64), + }, + { + name: 'Key 2', + publicKey: '2'.repeat(64), + }, +]; + +export class MockConnector implements Connector { + readonly id = 'mock'; + readonly name = 'Mock'; + readonly description = 'Connector for test purposes'; + + constructor() {} + + bindStore() {} + + async connectWallet() { + return { success: true }; + } + + async disconnectWallet() {} + + async getChainId() { + return { + chainId: mockChain.id, + }; + } + + async listKeys() { + return mockKeys; + } + + async isConnected() { + return { connected: true }; + } + + // @ts-ignore deliberate fail bb + async sendTransaction() { + return { + transactionHash: '0x' + 'a'.repeat(64), + signature: '0x' + 'a'.repeat(64), + sentAt: new Date().toISOString(), + receivedAt: new Date().toISOString(), + }; + } + + on() {} + + off() {} +} diff --git a/libs/wallet/src/connectors/snap-connector.ts b/libs/wallet/src/connectors/snap-connector.ts index e1a476d3e..318286f1a 100644 --- a/libs/wallet/src/connectors/snap-connector.ts +++ b/libs/wallet/src/connectors/snap-connector.ts @@ -1,247 +1,274 @@ +import EventEmitter from 'eventemitter3'; import { - WalletError, - type PubKey, - type Transaction, - type VegaConnector, -} from './vega-connector'; -import { clearConfig, setConfig } from '../storage'; + ConnectorError, + chainIdError, + connectError, + listKeysError, + noWalletError, + sendTransactionError, +} from '../errors'; +import { type Transaction } from '../transaction-types'; +import { + JsonRpcMethod, + type Connector, + type TransactionParams, + type VegaWalletEvent, +} from '../types'; + +enum EthereumMethod { + RequestSnaps = 'wallet_requestSnaps', + GetSnaps = 'wallet_getSnaps', + InvokeSnap = 'wallet_invokeSnap', +} + +type SnapConfig = { + node: string; + version: string; + snapId: string; +}; + +type SnapInvocationParams = Partial<{ + transaction: Transaction; + publicKey: string; + networkEndpoints: string[]; + sendingMode: 'TYPE_SYNC'; +}>; type RequestArguments = { method: string; params?: unknown[] | object; }; -type WindowEthereumProvider = { - isMetaMask: boolean; - request(args: RequestArguments): Promise; -}; -export const SnapConnectorErrors = { - ETHEREUM_UNDEFINED: new Error('MetaMask extension could not be found'), - NODE_ADDRESS_NOT_SET: new Error('nodeAddress is not set'), - SNAP_ID_NOT_SET: new Error('snapId is not set'), - TRANSACTION_PARSE: new Error('could not parse transaction data'), -}; - -const ethereumRequest = (args: RequestArguments): Promise => { - // can't declare `EthereumProvider` here because of the conflict with - // type definitions of `@web3-react` - if ( - 'ethereum' in window && - typeof window.ethereum === 'object' && - window.ethereum && - 'request' in window.ethereum && - 'isMetaMask' in window.ethereum && - window.ethereum.isMetaMask && - typeof window.ethereum.request === 'function' - ) { - return (window.ethereum as WindowEthereumProvider).request(args); - } - throw SnapConnectorErrors.ETHEREUM_UNDEFINED; -}; - -export const LOCAL_SNAP_ID = 'local:http://localhost:8080'; -export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap'; -export const DEFAULT_SNAP_VERSION = '1.0.1'; - -type GetSnapsResponse = Record; - -type Snap = { - id: string; - initialPermissions?: Record; - version: string; - enables: boolean; - blocked: boolean; -}; - -type InvokeSnapRequest = { - method: string; - params?: object; -}; - -type SendTransactionResponse = - | { - transactionHash: string; - receivedAt: string; - sentAt: string; - transaction?: { - signature?: { - value: string; - }; - }; - } - | { - error: Error & { - code: number; - data: unknown; - }; - }; -type GetChainIdResponse = { - chainID: string; -}; -type ListKeysResponse = { keys: PubKey[] }; - -/** - * 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 - */ -export const requestSnap = async ( - snapId: string, - params: Record<'version' | string, unknown> = {} -) => { - try { - await ethereumRequest({ - method: 'wallet_requestSnaps', - params: { - [snapId]: params, - }, - }); - } catch (err) { - // NOOP - rejected by user - } -}; - -/** - * Gets the list of all installed snaps. - * More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps - */ -export const getSnaps = async (): Promise => { - return (await ethereumRequest({ - method: 'wallet_getSnaps', - })) as GetSnapsResponse; -}; - -/** - * Gets the requested snap by `snapId` and an optional `version` - */ -export const getSnap = async ( - snapId: string, - version?: string -): Promise => { - const snaps = await getSnaps(); - return Object.values(snaps).find( - (snap) => snap.id === snapId && (!version || snap.version === version) - ); -}; - -export const invokeSnap = async ( - snapId: string, - request: InvokeSnapRequest -) => { - const req = { - method: 'wallet_invokeSnap', - params: { - snapId, - request, - }, +declare global { + type WindowEthereumProvider = { + isMetaMask: boolean; + request(args: RequestArguments): Promise; + selectedAddress: string | null; }; - return await ethereumRequest(req); -}; -export class SnapConnector implements VegaConnector { - description = "Connects using Vega Protocol's MetaMask snap"; - snapId: string | undefined = undefined; - nodeAddress: string | undefined = undefined; + interface Window { + // @ts-ignore must have identical modifiers + ethereum: WindowEthereumProvider; + } +} - // note we cannot set nodeAddress in the constructor because the - // trading app will not know what the vega url is until the app runs - constructor(snapId = DEFAULT_SNAP_ID) { - this.snapId = snapId; +export class SnapConnector implements Connector { + readonly id = 'snap'; + readonly name = 'MetaMask Snap'; + readonly description = + 'Connect directly via MetaMask with the Vega Snap for single key support without advanced features.'; + + 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 + constructor(config: SnapConfig) { + 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 { chainId } = await this.getChainId(); + + if (chainId !== desiredChainId) { + throw connectError( + `desired chain is ${desiredChainId} but wallet chain is ${chainId}` + ); + } + + this.startPoll(); + return { success: true }; + } catch (err) { + if (err instanceof ConnectorError) { + throw err; + } + + throw noWalletError(); + } + } + + async disconnectWallet() { + this.stopPoll(); + } + + // deprecated, pass chain on connect + async getChainId() { + try { + const res = await this.invokeSnap<{ chainID: string }>( + JsonRpcMethod.GetChainId, + { + networkEndpoints: [this.node], + } + ); + return { chainId: res.chainID }; + } catch (err) { + this.stopPoll(); + throw chainIdError(); + } } async listKeys() { - if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET; - return await invokeSnap(this.snapId, { - method: 'client.list_keys', - }); - } - - async connect() { - const res = await this.listKeys(); - setConfig({ - connector: 'snap', - token: null, // no token required for snap - url: null, // no url required for snap - }); - return res?.keys; - } - - async isAlive() { try { - const keys = await this.listKeys(); - if (keys.keys.length > 0) { - return true; + const res = await this.invokeSnap<{ + keys: Array<{ publicKey: string; name: string }>; + }>(JsonRpcMethod.ListKeys); + return res.keys; + } catch (err) { + this.stopPoll(); + throw listKeysError(); + } + } + + async isConnected() { + try { + // Check if metamask is unlocked + if (!window.ethereum.selectedAddress) { + throw noWalletError(); } - } catch (err) { - return false; - } - return false; + // 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 sendTx(pubKey: string, transaction: Transaction) { - if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET; - if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET; - - // This step is needed to strip the transaction object from any additional - // properties, such as `__proto__`, etc. - let txData = null; + async sendTransaction(params: TransactionParams) { try { - txData = JSON.parse(JSON.stringify(transaction)); + const res = await this.invokeSnap<{ + transactionHash: string; + transaction: { signature: { value: string } }; + receivedAt: string; + sentAt: string; + }>(JsonRpcMethod.SendTransaction, { + publicKey: params.publicKey, + sendingMode: params.sendingMode, + transaction: params.transaction, + networkEndpoints: [this.node], + }); + + return { + transactionHash: res.transactionHash, + signature: res.transaction.signature.value, + receivedAt: res.receivedAt, + sentAt: res.sentAt, + }; } catch (err) { - throw SnapConnectorErrors.TRANSACTION_PARSE; - } + if (err instanceof ConnectorError) { + throw err; + } - const payload = { - method: 'client.send_transaction', - params: { - sendingMode: 'TYPE_SYNC', - transaction: txData, - publicKey: pubKey, - networkEndpoints: [this.nodeAddress], + throw sendTransactionError(); + } + } + + on(event: VegaWalletEvent, callback: () => void) { + this.ee.on(event, callback); + } + + 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, }, - }; - - const result = await invokeSnap( - this.snapId, - payload - ); - - if ('error' in result) { - const { message, code, data } = result.error; - throw new WalletError( - message, - code, - typeof data === 'string' ? data : '' - ); - } - - if (!result?.transaction?.signature) { - throw new Error('could not retrieve transaction siganture'); - } - - return { - transactionHash: result.transactionHash, - signature: result?.transaction?.signature?.value, - receivedAt: result.receivedAt, - sentAt: result.sentAt, - }; - } - - async getChainId(): Promise { - if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET; - if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET; - - const response = await invokeSnap(this.snapId, { - method: 'client.get_chain_id', - params: { networkEndpoints: [this.nodeAddress] }, }); - - return response; } - async disconnect() { - clearConfig(); + // 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( + method: JsonRpcMethod, + params?: SnapInvocationParams + ): Promise { + return await this.request(EthereumMethod.InvokeSnap, { + snapId: this.snapId, + request: { + method, + params, + }, + }); + } + + /** + * Calls window.ethereum.request with method and params + */ + private async request( + method: EthereumMethod, + params?: object + ): Promise { + 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, + }); + } + + throw noWalletError(); } } diff --git a/libs/wallet/src/connectors/view-connector.tsx b/libs/wallet/src/connectors/view-connector.tsx deleted file mode 100644 index d285b8c6c..000000000 --- a/libs/wallet/src/connectors/view-connector.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import type { - PubKey, - TransactionResponse, - VegaConnector, -} from './vega-connector'; -import { clearConfig, getConfig, setConfig } from '../storage'; - -export class ViewConnector implements VegaConnector { - url: string | null; - pubkey: string | null | undefined = null; - - /** - * - */ - constructor(pubkey?: string | null) { - this.url = 'view-only'; - const cfg = getConfig(); - - if (pubkey || cfg?.token) { - this.pubkey = pubkey || cfg?.token; - } - } - setPubkey(pubkey: string) { - this.pubkey = pubkey; - } - connect(): Promise { - if (!this.pubkey) { - throw new Error('Cannot connect until address is set first'); - } - setConfig({ - token: this.pubkey, - connector: 'view', - url: this.url, - }); - return Promise.resolve([ - { - name: 'View only', - publicKey: this.pubkey, - }, - ]); - } - - async isAlive() { - return true; - } - - disconnect(): Promise { - clearConfig(); - this.pubkey = null; - return Promise.resolve(); - } - sendTx(): Promise { - throw new Error( - `You are connected in a view only state for public key: ${this.pubkey}. In order to send transactions you must connect to a real wallet.` - ); - } -} diff --git a/libs/wallet/src/connectors/view-party-connector.ts b/libs/wallet/src/connectors/view-party-connector.ts new file mode 100644 index 000000000..0a708691f --- /dev/null +++ b/libs/wallet/src/connectors/view-party-connector.ts @@ -0,0 +1,109 @@ +import { type StoreApi } from 'zustand'; +import { type Store, type Connector } from '../types'; +import { isValidVegaPublicKey } from '@vegaprotocol/utils'; +import { + ConnectorError, + chainIdError, + connectError, + listKeysError, + sendTransactionError, + userRejectedError, +} from '../errors'; +import { type TransactionResponse } from '../transaction-types'; + +export class ViewPartyConnector implements Connector { + readonly id = 'viewParty'; + readonly name = 'View as public key'; + readonly description = 'Provide a public key to connect in read only mode.'; + + store: StoreApi | undefined; + + constructor(pubKey?: string) { + if (pubKey) { + this.pubKey = pubKey; + } + } + + bindStore(store: StoreApi) { + this.store = store; + } + + async connectWallet() { + try { + if (this.pubKey) { + return { success: true }; + } + + const value = window.prompt('Enter public key'); + + if (value === null) { + throw userRejectedError(); + } + + if (!isValidVegaPublicKey(value)) { + throw connectError('invalid public key'); + } + + this.pubKey = value; + + return { success: true }; + } catch (err) { + if (err instanceof ConnectorError) { + throw err; + } + + throw connectError(); + } + } + + async disconnectWallet() { + this.pubKey = undefined; + } + + async getChainId(): Promise<{ chainId: string }> { + throw chainIdError('view party connector has not reference to a chain'); + } + + async listKeys() { + if (!this.pubKey) { + throw listKeysError(); + } + return [ + { + name: 'View only', + publicKey: this.pubKey, + }, + ]; + } + + async isConnected() { + if (this.pubKey) { + return { connected: true }; + } + + return { connected: false }; + } + + // define return type here to appease typescript, even though it'll only throw + async sendTransaction(): Promise { + throw sendTransactionError( + 'cannot send transactions when using view party connector' + ); + } + + on() { + console.warn('events are not supported using a view party connection'); + } + + off() { + console.warn('events are not supported using a view party connection'); + } + + get pubKey() { + return this.store?.getState().pubKey; + } + + set pubKey(value: string | undefined) { + this.store?.setState({ pubKey: value }); + } +} diff --git a/libs/wallet/src/context.ts b/libs/wallet/src/context.ts deleted file mode 100644 index bad8386bf..000000000 --- a/libs/wallet/src/context.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { createContext } from 'react'; -import type { - PubKey, - Transaction, - TransactionResponse, - VegaConnector, -} from './connectors'; - -export interface VegaWalletContextShape { - /** Current connected network */ - network: string; - - /** Url of current connected node */ - vegaUrl: string; - - /** Vega chain id */ - chainId: string; - - /** Url of running wallet service */ - vegaWalletServiceUrl: string; - - /** If the current connector does not support signing transactions */ - isReadOnly: boolean; - /** The current select public key */ - pubKey: string | null; - - /** Public keys stored in users wallet */ - pubKeys: PubKey[] | null; - - /** Calls connect on the supplied connector, storing the returned keys */ - connect: (connector: VegaConnector) => Promise; - - /** Disconnects from the connector and clears public key state */ - disconnect: () => Promise; - - /** Sets the current selected public key */ - selectPubKey: (pubKey: string) => void; - - /** Sign and send a transaction to the network */ - sendTx: ( - /** Public key to use in connected wallet */ - pubKey: string, - /** Transaction payload */ - transaction: Transaction - ) => Promise; - - /** Fetch public keys */ - fetchPubKeys?: () => Promise; - - /** Acknowledge disclaimer */ - acknowledgeNeeded?: boolean; - - /** Useful links for wallet users */ - links: { - explorer: string; - about: string; - concepts: string; - browserList: string; - chromeExtensionUrl: string; - mozillaExtensionUrl: string; - }; - - /** - * A flag determining whether the current connection is alive. - */ - isAlive: boolean | null; -} - -export const VegaWalletContext = createContext< - VegaWalletContextShape | undefined ->(undefined); diff --git a/libs/wallet/src/errors.ts b/libs/wallet/src/errors.ts new file mode 100644 index 000000000..bb77755c1 --- /dev/null +++ b/libs/wallet/src/errors.ts @@ -0,0 +1,104 @@ +export class ConnectorError extends Error { + code: number; + data: string | undefined; + + constructor(message: string, code: number, data?: string) { + super(message); + this.name = 'ConnectorError'; + this.code = code; + this.data = data; + } +} + +export const ConnectorErrors = { + userRejected: { message: 'user rejected', code: 0 }, + noConnector: { message: 'no connector', code: 1 }, + connect: { message: 'failed to connect', code: 2 }, + disconnect: { message: 'failed to disconnect', code: 3 }, + chainId: { message: 'incorrect chain id', code: 4 }, + listKeys: { message: 'failed to list keys', code: 5 }, + isConnected: { message: 'failed to check connection', code: 6 }, + sendTransaction: { message: 'failed to send transaction', code: 7 }, + unknown: { message: 'unknown error', code: 8 }, + noWallet: { message: 'no wallet running', code: 9 }, +}; + +export const userRejectedError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.userRejected.message, + ConnectorErrors.userRejected.code, + data + ); +}; + +export const noConnectorError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.noConnector.message, + ConnectorErrors.noConnector.code, + data + ); +}; + +export const connectError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.connect.message, + ConnectorErrors.connect.code, + data + ); +}; + +export const disconnectError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.disconnect.message, + ConnectorErrors.disconnect.code, + data + ); +}; + +export const chainIdError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.chainId.message, + ConnectorErrors.chainId.code, + data + ); +}; + +export const listKeysError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.listKeys.message, + ConnectorErrors.listKeys.code, + data + ); +}; + +export const isConnectedError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.isConnected.message, + ConnectorErrors.isConnected.code, + data + ); +}; + +export const sendTransactionError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.sendTransaction.message, + ConnectorErrors.sendTransaction.code, + data + ); +}; + +export const unknownError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.unknown.message, + ConnectorErrors.unknown.code, + data + ); +}; + +export const noWalletError = (data?: string) => { + return new ConnectorError( + ConnectorErrors.noWallet.message, + ConnectorErrors.noWallet.code, + data + ); +}; diff --git a/libs/wallet/src/index.ts b/libs/wallet/src/index.ts index 28ec16ab2..3ba2ab306 100644 --- a/libs/wallet/src/index.ts +++ b/libs/wallet/src/index.ts @@ -1,16 +1,22 @@ -export * from './connectors'; -export * from './context'; -export * from './use-vega-wallet'; -export * from './use-eager-connect'; -export * from './manage-dialog'; -export * from './provider'; -export * from './connect-dialog'; -export * from './utils'; -export * from './storage'; -export * from './use-chain-id'; +// Types +export * from './types'; +export * from './transaction-types'; + +// Core +export { mainnet, fairground, stagnet, mockChain, type Chain } from './chains'; +export { createConfig, coreStoreSlice, singleKeyStoreSlice } from './wallet'; + +// Connectors export { - useSimpleTransaction, - type Status, - type Result, - type Options, -} from './use-simple-transaction'; + InjectedConnector, + SnapConnector, + JsonRpcConnector, + ViewPartyConnector, + MockConnector, +} from './connectors'; + +// Errors +export * from './errors'; + +// Utils +export * from './utils'; diff --git a/libs/wallet/src/manage-dialog/manage-dialog.spec.tsx b/libs/wallet/src/manage-dialog/manage-dialog.spec.tsx deleted file mode 100644 index 32513e078..000000000 --- a/libs/wallet/src/manage-dialog/manage-dialog.spec.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable jest/no-conditional-expect */ -import { fireEvent, render, screen, within } from '@testing-library/react'; -import { VegaWalletContext } from '../context'; -import type { VegaWalletContextShape } from '../context'; -import type { VegaManageDialogProps } from '.'; -import { VegaManageDialog } from '.'; -import { truncateByChars } from '@vegaprotocol/utils'; - -let props: VegaManageDialogProps; -let context: Partial; -let pubKey1; -let pubKey2; - -beforeEach(() => { - pubKey1 = { publicKey: '111111__111111', name: 'test key 1' }; - pubKey2 = { publicKey: '222222__222222', name: 'test key 2' }; - props = { - dialogOpen: true, - setDialogOpen: jest.fn(), - }; - context = { - pubKey: pubKey1.publicKey, - pubKeys: [pubKey1, pubKey2], - selectPubKey: jest.fn(), - disconnect: jest.fn(), - }; -}); - -const generateJsx = ( - context: VegaWalletContextShape, - props: VegaManageDialogProps -) => { - return ( - - - - ); -}; - -it('Shows list of available keys and can disconnect', () => { - render(generateJsx(context as VegaWalletContextShape, props)); - - const list = screen.getByTestId('keypair-list'); - expect(list).toBeInTheDocument(); - // eslint-disable-next-line - expect(list.children).toHaveLength(context.pubKeys!.length); - - // eslint-disable-next-line - context.pubKeys!.forEach((pk, i) => { - const keyListItem = within(screen.getByTestId(`key-${pk.publicKey}`)); - expect( - keyListItem.getByText(truncateByChars(pk.publicKey)) - ).toBeInTheDocument(); - expect(keyListItem.getByText('Copy')).toBeInTheDocument(); - - // Active - // eslint-disable-next-line - if (pk.publicKey === context.pubKey!) { - expect(keyListItem.getByTestId('selected-key')).toBeInTheDocument(); - expect( - keyListItem.queryByTestId('select-keypair-button') - ).not.toBeInTheDocument(); - } - // Inactive - else { - const selectButton = keyListItem.getByTestId('select-keypair-button'); - expect(selectButton).toBeInTheDocument(); - expect(keyListItem.queryByTestId('selected-key')).not.toBeInTheDocument(); - fireEvent.click(selectButton); - expect(context.selectPubKey).toHaveBeenCalledWith(pk.publicKey); - } - }); - - // Disconnect - fireEvent.click(screen.getByTestId('disconnect')); - expect(context.disconnect).toHaveBeenCalled(); - expect(props.setDialogOpen).toHaveBeenCalledWith(false); -}); diff --git a/libs/wallet/src/provider.spec.tsx b/libs/wallet/src/provider.spec.tsx deleted file mode 100644 index 70bd4e621..000000000 --- a/libs/wallet/src/provider.spec.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; -import type { Transaction } from './connectors'; -import { ViewConnector, JsonRpcConnector } from './connectors'; -import { useVegaWallet } from './use-vega-wallet'; -import type { VegaWalletConfig } from './provider'; -import { VegaWalletProvider } from './provider'; -import { LocalStorage } from '@vegaprotocol/utils'; -import type { ReactNode } from 'react'; -import { WALLET_KEY } from './storage'; -import { DEFAULT_KEEP_ALIVE } from './use-is-alive'; - -const jsonRpcConnector = new JsonRpcConnector(); -const viewConnector = new ViewConnector(); - -const defaultConfig: VegaWalletConfig = { - network: 'TESTNET', - vegaUrl: 'https://vega.xyz', - vegaWalletServiceUrl: 'https://vegaservice.xyz', - links: { - explorer: 'explorer-link', - concepts: 'concepts-link', - chromeExtensionUrl: 'chrome-link', - mozillaExtensionUrl: 'mozilla-link', - }, - chainId: 'VEGA_CHAIN_ID', -}; - -const setup = (config?: Partial) => { - const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - - ); - return renderHook(() => useVegaWallet(), { wrapper }); -}; - -describe('VegaWalletProvider', () => { - afterAll(() => { - localStorage.clear(); - jest.useRealTimers(); - }); - - const mockPubKeys = [ - { publicKey: '111', name: 'public key 1' }, - { publicKey: '222', name: 'public key 2' }, - ]; - const spyOnConnect = jest - .spyOn(jsonRpcConnector, 'connect') - .mockImplementation(() => Promise.resolve(mockPubKeys)); - const spyOnSend = jest - .spyOn(jsonRpcConnector, 'sendTx') - .mockImplementation(() => - Promise.resolve({ - transactionHash: 'tsx', - sentAt: '', - receivedAt: '', - signature: '', - }) - ); - const storageSpy = jest.spyOn(LocalStorage, 'setItem'); - const spyOnDisconnect = jest - .spyOn(jsonRpcConnector, 'disconnect') - .mockImplementation(() => Promise.resolve()); - - it('connects, disconnects and retrieve keypairs', async () => { - const { result } = setup(); - - // Default state - expect(result.current).toEqual({ - network: defaultConfig.network, - vegaUrl: defaultConfig.vegaUrl, - chainId: defaultConfig.chainId, - vegaWalletServiceUrl: defaultConfig.vegaWalletServiceUrl, - acknowledgeNeeded: false, - pubKey: null, - pubKeys: null, - isReadOnly: false, - selectPubKey: expect.any(Function), - connect: expect.any(Function), - disconnect: expect.any(Function), - sendTx: expect.any(Function), - fetchPubKeys: expect.any(Function || undefined), - links: { - about: expect.any(String), - browserList: expect.any(String), - ...defaultConfig.links, - }, - isAlive: null, - }); - - // Connect - await act(async () => { - result.current.connect(jsonRpcConnector); - }); - expect(spyOnConnect).toHaveBeenCalled(); - expect(result.current.pubKeys).toHaveLength(mockPubKeys.length); - expect(result.current.pubKey).toBe(mockPubKeys[0].publicKey); - - // Change current pubkey - await act(async () => { - result.current.selectPubKey(mockPubKeys[1].publicKey); - }); - expect(result.current.pubKey).toBe(mockPubKeys[1].publicKey); - expect(storageSpy).toHaveBeenCalledWith( - WALLET_KEY, - mockPubKeys[1].publicKey - ); - - // Send tx - await act(async () => { - result.current.sendTx(mockPubKeys[1].publicKey, {} as Transaction); - }); - expect(spyOnSend).toHaveBeenCalledWith(mockPubKeys[1].publicKey, {}); - }); - - it('should fetch new keypairs', async () => { - const { result } = setup(); - - // Default state - expect(result.current.pubKey).toEqual(null); - - // Connect - await act(async () => { - result.current.connect(jsonRpcConnector); - result.current.selectPubKey(mockPubKeys[0].publicKey); - }); - expect(spyOnConnect).toHaveBeenCalled(); - expect(result.current.pubKeys).toHaveLength(mockPubKeys.length); - expect(result.current.pubKey).toBe(mockPubKeys[0].publicKey); - - // Fetch pub keys - mockPubKeys.push({ publicKey: '333', name: 'public key 3' }); - await act(async () => { - result.current.fetchPubKeys && result.current.fetchPubKeys(); - }); - expect(result.current.pubKeys).toHaveLength(mockPubKeys.length); - }); - - it('persists selected pubkey and disconnects', async () => { - const { result } = setup(); - expect(result.current.pubKey).toBe(null); - - await act(async () => { - result.current.connect(jsonRpcConnector); - result.current.selectPubKey(mockPubKeys[0].publicKey); - }); - - expect(result.current.pubKey).toBe(mockPubKeys[0].publicKey); - - // Disconnect - await act(async () => { - result.current.disconnect(); - }); - expect(result.current.pubKey).toBe(null); - expect(result.current.pubKeys).toBe(null); - expect(spyOnDisconnect).toHaveBeenCalled(); - expect(localStorage.getItem(WALLET_KEY)).toBe(null); - }); - - it('sets isReadOnly to true if using view connector', async () => { - jest - .spyOn(viewConnector, 'connect') - .mockImplementation(() => Promise.resolve(mockPubKeys)); - const { result } = setup(); - expect(result.current.pubKey).toBe(null); - - await act(async () => { - result.current.connect(viewConnector); - }); - expect(result.current.isReadOnly).toBe(true); - }); - - it('acknowledgeNeeded will set on', async () => { - jest - .spyOn(viewConnector, 'connect') - .mockImplementation(() => Promise.resolve(mockPubKeys)); - - const { result } = setup({ network: 'MAINNET' }); - - expect(result.current.acknowledgeNeeded).toBe(true); - - await act(async () => { - result.current.connect(viewConnector); - }); - expect(result.current.isReadOnly).toBe(true); - }); - - it('sets isAlive to false if connection lost', async () => { - // setup and connect - jest.useFakeTimers(); - jest - .spyOn(jsonRpcConnector, 'isAlive') - .mockImplementation(() => Promise.resolve(true)); - const { result } = setup(); - await act(async () => { - result.current.connect(jsonRpcConnector); - }); - expect(result.current.isAlive).toEqual(null); - - // loose connection - jest - .spyOn(jsonRpcConnector, 'isAlive') - .mockImplementation(() => Promise.resolve(false)); - jest.advanceTimersByTime(DEFAULT_KEEP_ALIVE); - - await waitFor(() => { - expect(result.current.isAlive).toEqual(false); - }); - }); -}); diff --git a/libs/wallet/src/provider.tsx b/libs/wallet/src/provider.tsx deleted file mode 100644 index b82797d55..000000000 --- a/libs/wallet/src/provider.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { LocalStorage } from '@vegaprotocol/utils'; -import { - useCallback, - useMemo, - useRef, - useState, - type ReactNode, - useEffect, -} from 'react'; -import { WalletClientError } from '@vegaprotocol/wallet-client'; -import { type VegaWalletContextShape } from '.'; -import { - type PubKey, - type VegaConnector, - type Transaction, -} from './connectors/vega-connector'; -import { VegaWalletContext } from './context'; -import { WALLET_KEY, WALLET_RISK_ACCEPTED_KEY } from './storage'; -import { ViewConnector } from './connectors'; -import { useLocalStorage } from '@vegaprotocol/react-helpers'; -import { DEFAULT_KEEP_ALIVE, useIsAlive } from './use-is-alive'; - -type Networks = - | 'MAINNET' - | 'MAINNET_MIRROR' - | 'TESTNET' - | 'VALIDATOR_TESTNET' - | 'STAGNET1' - | 'DEVNET' - | 'CUSTOM'; - -interface VegaWalletLinks { - explorer: string; - concepts: string; - chromeExtensionUrl: string; - mozillaExtensionUrl: string; -} - -export interface VegaWalletConfig { - network: Networks; - vegaUrl: string; - chainId: string; - vegaWalletServiceUrl: string; - links: VegaWalletLinks; - keepAlive?: number; -} - -const ExternalLinks = { - VEGA_WALLET_URL_ABOUT: 'https://vega.xyz/wallet/#overview', - VEGA_WALLET_BROWSER_LIST: '', -}; - -interface VegaWalletProviderProps { - children: ReactNode; - config: VegaWalletConfig; -} - -export const VegaWalletProvider = ({ - children, - config, -}: VegaWalletProviderProps) => { - // Current selected pubKey - const [pubKey, setPubKey] = useState(null); - const [isReadOnly, setIsReadOnly] = useState(false); - - // Array of public keys retrieved from the connector - const [pubKeys, setPubKeys] = useState(null); - - // Reference to the current connector instance - const connector = useRef(null); - - const selectPubKey = useCallback((pk: string) => { - setPubKey(pk); - LocalStorage.setItem(WALLET_KEY, pk); - }, []); - - const fetchPubKeys = useCallback(async () => { - if (!connector.current) { - throw new Error('No connector'); - } - try { - const keys = await connector.current.connect(); - - if (keys?.length) { - setPubKeys(keys); - setIsReadOnly(connector.current instanceof ViewConnector); - return keys; - } else { - return null; - } - } catch (err) { - if (err instanceof WalletClientError) { - throw err; - } - return null; - } - }, []); - - const connect = useCallback(async (c: VegaConnector) => { - connector.current = c; - try { - const keys = await connector.current.connect(); - - if (keys?.length) { - setPubKeys(keys); - setIsReadOnly(connector.current instanceof ViewConnector); - const lastUsedPubKey = LocalStorage.getItem(WALLET_KEY); - const foundKey = keys.find((key) => key.publicKey === lastUsedPubKey); - if (foundKey) { - setPubKey(foundKey.publicKey); - } else { - setPubKey(keys[0].publicKey); - } - - return keys; - } else { - return null; - } - } catch (err) { - if (err instanceof WalletClientError) { - throw err; - } - return null; - } - }, []); - - const disconnect = useCallback(async () => { - // always clear state after attempted disconnection.. this - // is because long lived token sessions (used in tests) - // cannot be cleared. Clearing state will force user to reconnect - // again as expected - setPubKeys(null); - setPubKey(null); - setIsReadOnly(false); - LocalStorage.removeItem(WALLET_KEY); - try { - await connector.current?.disconnect(); - connector.current = null; - } catch (err) { - console.error(err); - connector.current = null; - } - }, []); - - const sendTx = useCallback((pubkey: string, transaction: Transaction) => { - if (!connector.current) { - throw new Error('No connector'); - } - return connector.current.sendTx(pubkey, transaction); - }, []); - - const [riskAcceptedValue] = useLocalStorage(WALLET_RISK_ACCEPTED_KEY); - const acknowledgeNeeded = - config.network === 'MAINNET' && riskAcceptedValue !== 'true'; - - const isAlive = useIsAlive( - connector.current && pubKey ? connector.current : null, - config.keepAlive != null ? config.keepAlive : DEFAULT_KEEP_ALIVE - ); - /** - * Force disconnect if connected and wallet is unreachable. - */ - useEffect(() => { - if (isAlive === false) { - disconnect(); - } - }, [disconnect, isAlive]); - - const contextValue = useMemo(() => { - return { - vegaUrl: config.vegaUrl, - chainId: config.chainId, - vegaWalletServiceUrl: config.vegaWalletServiceUrl, - network: config.network, - links: { - explorer: config.links.explorer, - about: ExternalLinks.VEGA_WALLET_URL_ABOUT, - browserList: ExternalLinks.VEGA_WALLET_BROWSER_LIST, - concepts: config.links.concepts, - chromeExtensionUrl: config.links.chromeExtensionUrl, - mozillaExtensionUrl: config.links.mozillaExtensionUrl, - }, - isReadOnly, - pubKey, - pubKeys, - selectPubKey, - connect, - disconnect, - sendTx, - fetchPubKeys, - acknowledgeNeeded, - isAlive, - }; - }, [ - config, - isReadOnly, - pubKey, - pubKeys, - selectPubKey, - connect, - disconnect, - sendTx, - fetchPubKeys, - acknowledgeNeeded, - isAlive, - ]); - - return ( - - {children} - - ); -}; diff --git a/libs/wallet/src/setup-tests.ts b/libs/wallet/src/setup-tests.ts index 79af8900a..7b0828bfa 100644 --- a/libs/wallet/src/setup-tests.ts +++ b/libs/wallet/src/setup-tests.ts @@ -1,17 +1 @@ import '@testing-library/jest-dom'; -import ResizeObserver from 'resize-observer-polyfill'; -import { locales } from '@vegaprotocol/i18n'; -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -// Set up i18n instance so that components have the correct default -// en translations -i18n.use(initReactI18next).init({ - // we init with resources - resources: locales, - fallbackLng: 'en', - ns: ['wallet'], - defaultNS: 'wallet', -}); - -global.ResizeObserver = ResizeObserver; diff --git a/libs/wallet/src/storage.ts b/libs/wallet/src/storage.ts deleted file mode 100644 index 238775a34..000000000 --- a/libs/wallet/src/storage.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { LocalStorage } from '@vegaprotocol/utils'; - -interface ConnectorConfig { - token: string | null; - connector: 'injected' | 'jsonRpc' | 'view' | 'snap'; - url: string | null; -} - -export const WALLET_CONFIG = 'vega_wallet_config'; -export const WALLET_KEY = 'vega_wallet_key'; -export const WALLET_RISK_ACCEPTED_KEY = 'vega_wallet_risk_accepted'; - -export function setConfig(cfg: ConnectorConfig) { - LocalStorage.setItem(WALLET_CONFIG, JSON.stringify(cfg)); -} - -export function getConfig(): ConnectorConfig | null { - const cfg = LocalStorage.getItem(WALLET_CONFIG); - if (cfg) { - try { - return JSON.parse(cfg); - } catch { - return null; - } - } else { - return null; - } -} - -export function clearConfig() { - LocalStorage.removeItem(WALLET_CONFIG); -} - -export function getAcknowledged() { - return LocalStorage.getItem(WALLET_RISK_ACCEPTED_KEY) === 'true'; -} - -export function setAcknowledged() { - return LocalStorage.setItem(WALLET_RISK_ACCEPTED_KEY, 'true'); -} diff --git a/libs/wallet/src/test-helpers.ts b/libs/wallet/src/test-helpers.ts index 2966939fd..00fc63713 100644 --- a/libs/wallet/src/test-helpers.ts +++ b/libs/wallet/src/test-helpers.ts @@ -1,7 +1,7 @@ export function mockBrowserWallet(overrides?: Partial) { const vega: Vega = { - connectWallet: jest.fn().mockReturnValue(Promise.resolve(null)), - disconnectWallet: jest.fn().mockReturnValue(Promise.resolve()), + connectWallet: jest.fn().mockResolvedValue(null), + disconnectWallet: jest.fn().mockResolvedValue(undefined), listKeys: jest .fn() .mockReturnValue({ keys: [{ name: 'test key', publicKey: '0x123' }] }), @@ -13,7 +13,9 @@ export function mockBrowserWallet(overrides?: Partial) { success: true, txHash: '0x123', }), + getChainId: jest.fn().mockResolvedValue({ chainID: 'mock chain' }), on: jest.fn(), + off: jest.fn(), isConnected: jest.fn().mockRejectedValue(Promise.resolve(true)), ...overrides, }; diff --git a/libs/wallet/src/connectors/vega-connector.ts b/libs/wallet/src/transaction-types.ts similarity index 78% rename from libs/wallet/src/connectors/vega-connector.ts rename to libs/wallet/src/transaction-types.ts index 0828c7fad..b07f16e11 100644 --- a/libs/wallet/src/connectors/vega-connector.ts +++ b/libs/wallet/src/transaction-types.ts @@ -1,6 +1,13 @@ -import { WalletClientError } from '@vegaprotocol/wallet-client'; -import type * as Schema from '@vegaprotocol/types'; -import type { PeggedReference } from '@vegaprotocol/types'; +import type { + AccountType, + DispatchMetric, + PeggedReference, + OrderType, + Side, + OrderTimeInForce, + StopOrderExpiryStrategy, + VoteValue, +} from '@vegaprotocol/types'; export interface LiquidityProvisionSubmission { liquidityProvisionSubmission: LiquidityProvisionBody; @@ -39,9 +46,9 @@ export interface UndelegateSubmissionBody { export interface OrderSubmission { marketId: string; reference?: string; - type: Schema.OrderType; - side: Schema.Side; - timeInForce: Schema.OrderTimeInForce; + type: OrderType; + side: Side; + timeInForce: OrderTimeInForce; size: string; price?: string; expiresAt?: string; @@ -62,7 +69,7 @@ export interface OrderAmendment { marketId: string; orderId: string; reference?: string; - timeInForce: Schema.OrderTimeInForce; + timeInForce: OrderTimeInForce; sizeDelta?: number; price?: string; expiresAt?: string; @@ -82,7 +89,7 @@ export interface OrderAmendmentBody { export interface StopOrderSetup { orderSubmission: OrderSubmission; expiresAt?: string; - expiryStrategy?: Schema.StopOrderExpiryStrategy; + expiryStrategy?: StopOrderExpiryStrategy; price?: string; trailingPercentOffset?: string; } @@ -107,7 +114,7 @@ export interface StopOrdersCancellationBody { export interface VoteSubmissionBody { voteSubmission: { - value: Schema.VoteValue; + value: VoteValue; proposalId: string; }; } @@ -400,9 +407,9 @@ export interface BatchMarketInstructionSubmissionBody { } interface TransferBase { - fromAccountType: Schema.AccountType; + fromAccountType: AccountType; to: string; - toAccountType: Schema.AccountType; + toAccountType: AccountType; asset: string; amount: string; reference?: string; @@ -421,7 +428,7 @@ export interface RecurringTransfer extends TransferBase { endEpoch?: number; dispatchStrategy?: { assetForMetric: string; - metric: Schema.DispatchMetric; + metric: DispatchMetric; markets?: string[]; }; }; @@ -485,7 +492,6 @@ export interface UpdateMarginMode { export interface UpdateMarginModeBody { updateMarginMode: UpdateMarginMode; } - export type Transaction = | UpdateMarginModeBody | StopOrdersSubmissionBody @@ -506,91 +512,20 @@ export type Transaction = | CreateReferralSet | UpdateReferralSet; -export const isMarginModeUpdateTransaction = ( - transaction: Transaction -): transaction is UpdateMarginModeBody => 'updateMarginMode' in transaction; - -export const isWithdrawTransaction = ( - transaction: Transaction -): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction; - -export const isOrderSubmissionTransaction = ( - transaction: Transaction -): transaction is OrderSubmissionBody => 'orderSubmission' in transaction; - -export const isOrderCancellationTransaction = ( - transaction: Transaction -): transaction is OrderCancellationBody => 'orderCancellation' in transaction; - -export const isStopOrdersSubmissionTransaction = ( - transaction: Transaction -): transaction is StopOrdersSubmissionBody => - 'stopOrdersSubmission' in transaction; - -export const isStopOrdersCancellationTransaction = ( - transaction: Transaction -): transaction is StopOrdersCancellationBody => - 'stopOrdersCancellation' in transaction; - -export const isOrderAmendmentTransaction = ( - transaction: Transaction -): transaction is OrderAmendmentBody => 'orderAmendment' in transaction; - -export const isBatchMarketInstructionsTransaction = ( - transaction: Transaction -): transaction is BatchMarketInstructionSubmissionBody => - 'batchMarketInstructions' in transaction; - -export const isTransferTransaction = ( - transaction: Transaction -): transaction is TransferBody => 'transfer' in transaction; - -export const isReferralRelatedTransaction = ( - transaction: Transaction -): transaction is CreateReferralSet | ApplyReferralCode => - 'createReferralSet' in transaction || 'applyReferralCode' in transaction; - export interface TransactionResponse { transactionHash: string; signature: string; // still to be added by core receivedAt: string; sentAt: string; } -export class WalletError extends WalletClientError { - data: string; - constructor(message: string, code: number, data = 'Wallet error') { - super({ code, message, data }); - this.data = data; - } -} - -export interface PubKey { - publicKey: string; - name: string; -} - -export interface VegaConnector { - url?: string | null; - - /** Connect to wallet and return keys */ - connect(): Promise; - - /** Disconnect from wallet */ - disconnect(): Promise; - - /** - * sign and send a transaction to the network - * - * @returns promise containing either the transaction payload or null if the user rejected the request - */ - sendTx: ( - pubkey: string, - transaction: Transaction - ) => Promise; - - /** - * Checks if the connection to the connector is alive. - */ - isAlive: () => Promise; -} +// TODO: use this? +// +// export class WalletError extends WalletClientError { +// data: string; +// +// constructor(message: string, code: number, data = 'Wallet error') { +// super({ code, message, data }); +// this.data = data; +// } +// } diff --git a/libs/wallet/src/types.ts b/libs/wallet/src/types.ts new file mode 100644 index 000000000..d3d0ad5fe --- /dev/null +++ b/libs/wallet/src/types.ts @@ -0,0 +1,130 @@ +import { type StoreApi } from 'zustand'; +import { + type Transaction, + type TransactionResponse, +} from './transaction-types'; +import { type Chain } from './chains'; +import { type ConnectorError } from './errors'; + +export enum JsonRpcMethod { + ConnectWallet = 'client.connect_wallet', + DisconnectWallet = 'client.disconnect_wallet', + ListKeys = 'client.list_keys', + SignTransaction = 'client.sign_transaction', + SendTransaction = 'client.send_transaction', + GetChainId = 'client.get_chain_id', +} + +export interface TransactionParams { + publicKey: string; + transaction: Transaction; + sendingMode: 'TYPE_SYNC'; +} + +export type VegaWalletEvent = 'client.disconnected'; + +export type ConnectorType = + | 'injected' + | 'jsonRpc' + | 'snap' + | 'viewParty' + | 'mock'; + +export interface Connector { + readonly id: ConnectorType; + readonly name: string; + readonly description: string; + + bindStore(state: StoreApi): void; + connectWallet(chainId?: string): Promise<{ success: boolean }>; + disconnectWallet(): Promise; + getChainId(): Promise<{ chainId: string }>; + listKeys(): Promise>; + isConnected(): Promise<{ connected: boolean }>; + sendTransaction(params: TransactionParams): Promise; + on(event: VegaWalletEvent, callback: () => void): void; + off(event: VegaWalletEvent, callback?: () => void): void; +} + +export type Key = { + publicKey: string; + name: string; +}; + +export type Status = 'disconnected' | 'connecting' | 'connected'; + +export type CoreStore = { + chainId: string; + status: Status; + current: ConnectorType | undefined; + keys: Key[]; + error: ConnectorError | undefined; + jsonRpcToken: string | undefined; +}; + +export type SingleKeyStore = { + pubKey: string | undefined; +}; + +export type Store = CoreStore & SingleKeyStore; + +export type Config = { + chains: Chain[]; + defaultChainId: string; + connectors: Connector[]; +}; + +export type Wallet = { + store: StoreApi; + connectors: Connector[]; + connect: (id: ConnectorType) => Promise<{ status: Status }>; + disconnect: () => Promise<{ status: Status }>; + refreshKeys: () => Promise; + sendTransaction: (params: TransactionParams) => Promise; + reset: () => void; +}; + +declare global { + interface Vega { + connectWallet: (args: { chainId: string }) => Promise; + disconnectWallet: () => Promise; + listKeys: () => Promise<{ + keys: Array<{ name: string; publicKey: string }>; + }>; + sendTransaction: (params: { + publicKey: string; + transaction: Transaction; + sendingMode: 'TYPE_SYNC'; + }) => Promise<{ + receivedAt: string; + sentAt: string; + transaction: { + from: { + pubKey: string; + }; + inputData: string; + pow: { + tid: string; + nonce: string; + }; + signature: { + algo: string; + value: string; + version: number; + }; + version: number; + }; + transactionHash: string; + }>; + + on: (event: VegaWalletEvent, callback: () => void) => void; + off: (event: VegaWalletEvent, callback: () => void) => void; + isConnected: () => Promise; + // deprecated + getChainId: () => Promise<{ chainID: string }>; + } + + interface Window { + vega: Vega; + } +} diff --git a/libs/wallet/src/use-chain-id.spec.ts b/libs/wallet/src/use-chain-id.spec.ts deleted file mode 100644 index b0a4f400b..000000000 --- a/libs/wallet/src/use-chain-id.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { renderHook, waitFor } from '@testing-library/react'; -import { MAX_FETCH_ATTEMPTS, useChainId } from './use-chain-id'; - -global.fetch = jest.fn(); -const mockFetch = global.fetch as jest.Mock; -mockFetch.mockImplementation((url: string) => { - return Promise.resolve({ ok: true }); -}); - -describe('useChainId', () => { - it('does not call fetch when statistics url could not be determined', async () => { - renderHook(() => useChainId('')); - await waitFor(() => { - expect(mockFetch).toHaveBeenCalledTimes(0); - }); - }); - - it('calls fetch with correct statistics url', async () => { - renderHook(() => useChainId('http://localhost:1234/graphql')); - await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith( - 'http://localhost:1234/statistics' - ); - }); - }); - - it('does not return chain id when chain id is not present in response', async () => { - const { result } = renderHook(() => - useChainId('http://localhost:1234/graphql') - ); - await waitFor(() => { - expect(result.current).toBeUndefined(); - }); - }); - - it('returns chain id when chain id is present in response', async () => { - mockFetch.mockImplementation(() => { - return Promise.resolve({ - ok: true, - json: () => ({ - statistics: { - chainId: '1234', - }, - }), - }); - }); - const { result } = renderHook(() => - useChainId('http://localhost:1234/graphql') - ); - await waitFor(() => { - expect(result.current).not.toBeUndefined(); - expect(result.current).toEqual('1234'); - }); - }); - - it('returns chain id when within max number of attempts', async () => { - mockFetch.mockImplementation(() => { - if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS) { - return Promise.reject(); - } - return Promise.resolve({ - ok: true, - json: () => ({ - statistics: { - chainId: '1234', - }, - }), - }); - }); - const { result } = renderHook(() => - useChainId('http://localhost:1234/graphql') - ); - await waitFor(() => { - expect(result.current).not.toBeUndefined(); - expect(result.current).toEqual('1234'); - }); - }); - - it('does not return chain id when max number of attempts exceeded', async () => { - mockFetch.mockImplementation(() => { - if (mockFetch.mock.calls.length < MAX_FETCH_ATTEMPTS + 10) { - return Promise.reject(); - } - return Promise.resolve({ - ok: true, - json: () => ({ - statistics: { - chainId: '1234', - }, - }), - }); - }); - const { result } = renderHook(() => - useChainId('http://localhost:5678/graphql') - ); - await waitFor(() => { - expect(result.current).toBeUndefined(); - }); - }); -}); diff --git a/libs/wallet/src/use-chain-id.ts b/libs/wallet/src/use-chain-id.ts deleted file mode 100644 index 2c280ad8f..000000000 --- a/libs/wallet/src/use-chain-id.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useState } from 'react'; - -export const MAX_FETCH_ATTEMPTS = 3; - -const cache: Record = {}; - -/** - * Gets the statistics url for a given vega url. - * Example: - * https://graphql.example.com/graphql -> https://graphql.example.com/statistics - */ -const getNodeStatisticsUrl = (vegaUrl: string) => { - try { - const url = new URL(vegaUrl); - url.pathname = 'statistics'; - return url.toString(); - } catch (err) { - return undefined; - } -}; - -export const useChainId = (vegaUrl: string | undefined) => { - const [chainId, setChainId] = useState( - vegaUrl ? cache[vegaUrl] : undefined - ); - const [fetchAttempts, setFetchAttempts] = useState(1); - - const statisticsUrl = vegaUrl ? getNodeStatisticsUrl(vegaUrl) : undefined; - - useEffect(() => { - // abort when `/statistics` URL could not be determined - if (!statisticsUrl || !vegaUrl) return; - let isCancelled = false; - if (cache[vegaUrl]) { - setChainId(cache[vegaUrl]); - return; - } - fetch(statisticsUrl) - .then((response) => response.json()) - .then((response) => { - if (isCancelled) { - return; - } - if (!response?.statistics?.chainId) { - throw new Error('statistics.chainId not present in fetched response'); - } - - const chainId = response.statistics.chainId; - cache[vegaUrl] = chainId; - setChainId(chainId); - }) - .catch(() => { - if (fetchAttempts < MAX_FETCH_ATTEMPTS) { - setFetchAttempts((value) => (value += 1)); - } - }); - return () => { - isCancelled = true; - }; - }, [fetchAttempts, statisticsUrl, vegaUrl]); - return chainId; -}; diff --git a/libs/wallet/src/use-eager-connect.ts b/libs/wallet/src/use-eager-connect.ts deleted file mode 100644 index c430a7a9a..000000000 --- a/libs/wallet/src/use-eager-connect.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from 'react'; -import { InjectedConnector } from './connectors/injected-connector'; -import { SnapConnector } from './connectors/snap-connector'; -import { getConfig } from './storage'; -import type { Connectors } from './connectors'; -import { useVegaWallet } from './use-vega-wallet'; - -export function useEagerConnect(connectors: Connectors) { - const [connecting, setConnecting] = useState(true); - const { vegaUrl, chainId, connect, acknowledgeNeeded } = useVegaWallet(); - - useEffect(() => { - const attemptConnect = async () => { - const cfg = getConfig(); - // No stored config, or config was malformed or no risk accepted - if (!cfg || !cfg.connector || acknowledgeNeeded) { - setConnecting(false); - return; - } - - // Use the connector string in local storage to find the right connector to auto - // connect to - const connector = connectors[cfg.connector]; - - // Developer hasn't provided this connector - if (!connector) { - setConnecting(false); - console.error( - `Can't eager connect, connector: ${cfg.connector} not found` - ); - return; - } - - try { - if (connector instanceof InjectedConnector) { - await connector.connectWallet(chainId); - await connect(connector); - } else if (connector instanceof SnapConnector) { - connector.nodeAddress = new URL(vegaUrl).origin; - await connect(connector); - } else { - await connect(connector); - } - } catch { - console.warn(`Failed to connect with connector: ${cfg.connector}`); - } finally { - setConnecting(false); - } - }; - - if (typeof window !== 'undefined') { - attemptConnect(); - } - }, [connect, connectors, acknowledgeNeeded, vegaUrl, chainId]); - - return connecting; -} diff --git a/libs/wallet/src/use-injected-connector.spec.tsx b/libs/wallet/src/use-injected-connector.spec.tsx deleted file mode 100644 index 0a1a72a04..000000000 --- a/libs/wallet/src/use-injected-connector.spec.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; -import { Status, useInjectedConnector } from './use-injected-connector'; -import type { ReactNode } from 'react'; -import type { VegaWalletConfig } from './provider'; -import { VegaWalletProvider } from './provider'; -import { InjectedConnector } from './connectors'; -import { mockBrowserWallet } from './test-helpers'; - -const defaultConfig: VegaWalletConfig = { - network: 'TESTNET', - vegaUrl: 'https://vega.xyz', - vegaWalletServiceUrl: 'https://vegaservice.xyz', - links: { - explorer: 'explorer-link', - concepts: 'concepts-link', - chromeExtensionUrl: 'chrome-link', - mozillaExtensionUrl: 'mozilla-link', - }, - chainId: 'VEGA_CHAIN_ID', -}; - -const setup = (callback = jest.fn(), config?: Partial) => { - const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - - ); - return renderHook(() => useInjectedConnector(callback), { wrapper }); -}; - -const injected = new InjectedConnector(); - -describe('useInjectedConnector', () => { - it('attempts connection', async () => { - const { result } = setup(); - expect(typeof result.current.connect).toBe('function'); - expect(result.current.status).toBe(Status.Idle); - expect(result.current.error).toBe(null); - }); - - it('errors if vega not injected', async () => { - const { result } = setup(); - await act(async () => { - result.current.connect(injected, '1'); - }); - expect(result.current.error?.message).toBe('window.vega not found'); - expect(result.current.status).toBe(Status.Error); - }); - - it('errors if connection throws', async () => { - const callback = jest.fn(); - mockBrowserWallet({ - connectWallet: jest.fn().mockReturnValue(Promise.reject()), - }); - const { result } = setup(callback); - - await act(async () => { - result.current.connect(injected, '1'); // default mock chainId is '1' - }); - expect(result.current.status).toBe(Status.Error); - expect(result.current.error?.message).toBe( - 'could not connect to the vega wallet on chain: 1' - ); - }); - - it('connects', async () => { - const callback = jest.fn(); - const vega = mockBrowserWallet(); - const { result } = setup(callback); - - act(() => { - result.current.connect(injected, '1'); // default mock chainId is '1' - }); - - await waitFor(() => { - expect(vega.connectWallet).toHaveBeenCalled(); - expect(vega.listKeys).toHaveBeenCalled(); - }); - - expect(result.current.status).toBe(Status.Connected); - expect(callback).toHaveBeenCalled(); - }); - - it('connects when aknowledgement required', async () => { - const callback = jest.fn(); - const vega = mockBrowserWallet(); - const { result } = setup(callback, { network: 'MAINNET' }); - - act(() => { - result.current.connect(injected, '1'); // default mock chainId is '1' - }); - - await waitFor(() => { - expect(vega.listKeys).toHaveBeenCalled(); - }); - - expect(result.current.status).toBe(Status.AcknowledgeNeeded); - expect(callback).not.toHaveBeenCalled(); - }); -}); diff --git a/libs/wallet/src/use-injected-connector.ts b/libs/wallet/src/use-injected-connector.ts deleted file mode 100644 index 094d3052c..000000000 --- a/libs/wallet/src/use-injected-connector.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback, useState } from 'react'; -import { - InjectedConnectorErrors, - SnapConnector, - SnapConnectorErrors, -} from './connectors'; -import { InjectedConnector } from './connectors'; -import { useVegaWallet } from './use-vega-wallet'; - -export enum Status { - Idle = 'Idle', - GettingChainId = 'GettingChainId', - Connecting = 'Connecting', - Connected = 'Connected', - Error = 'Error', - AcknowledgeNeeded = 'AcknowledgeNeeded', -} - -export const useInjectedConnector = (onConnect: () => void) => { - const { vegaUrl, connect, acknowledgeNeeded } = useVegaWallet(); - const [status, setStatus] = useState(Status.Idle); - const [error, setError] = useState(null); - - const attemptConnect = useCallback( - async ( - connector: InjectedConnector | SnapConnector, - appChainId: string - ) => { - try { - if (connector instanceof InjectedConnector && !('vega' in window)) { - throw InjectedConnectorErrors.VEGA_UNDEFINED; - } - if (connector instanceof SnapConnector) { - if (!('ethereum' in window)) { - throw SnapConnectorErrors.ETHEREUM_UNDEFINED; - } - if (!vegaUrl) { - throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET; - } - connector.nodeAddress = new URL(vegaUrl).origin; - } - - // check the chain id for snap connector - if (connector instanceof SnapConnector) { - setStatus(Status.GettingChainId); - const { chainID } = await connector.getChainId(); - if (chainID !== appChainId) { - throw InjectedConnectorErrors.INVALID_CHAIN; - } - } - - setStatus(Status.Connecting); - if (connector instanceof InjectedConnector) { - // extra step for injected connector - authorize wallet - await connector.connectWallet(appChainId); - } - await connect(connector); // connect with keys - - if (acknowledgeNeeded) { - setStatus(Status.AcknowledgeNeeded); - } else { - setStatus(Status.Connected); - onConnect(); - } - } catch (err) { - if (err instanceof Error) { - setError(err); - } else { - setError(new Error('injected connection failed')); - } - setStatus(Status.Error); - } - }, - [vegaUrl, acknowledgeNeeded, connect, onConnect] - ); - - return { - status, - error, - connect: attemptConnect, - }; -}; diff --git a/libs/wallet/src/use-is-alive.ts b/libs/wallet/src/use-is-alive.ts deleted file mode 100644 index f13a51534..000000000 --- a/libs/wallet/src/use-is-alive.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react'; -import { type VegaConnector } from '.'; - -/** - * Determines the interval for checking if wallet connection is alive. - */ -export const DEFAULT_KEEP_ALIVE = 1000; - -export const useIsAlive = ( - connector: VegaConnector | null, - interval: number -) => { - const [alive, setAlive] = useState(null); - - useEffect(() => { - if (!connector) { - return; - } - - const i = setInterval(() => { - connector.isAlive().then((isAlive) => { - if (alive !== isAlive) setAlive(isAlive); - }); - }, interval); - return () => { - clearInterval(i); - }; - }, [alive, connector, interval]); - - return alive; -}; diff --git a/libs/wallet/src/use-is-wallet-service-running.spec.ts b/libs/wallet/src/use-is-wallet-service-running.spec.ts deleted file mode 100644 index a5dc49297..000000000 --- a/libs/wallet/src/use-is-wallet-service-running.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { renderHook, waitFor } from '@testing-library/react'; -import { JsonRpcConnector } from './connectors'; -import { useIsWalletServiceRunning } from './use-is-wallet-service-running'; - -describe('useIsWalletServiceRunning', () => { - it('returns true if wallet is running', async () => { - const url = 'https://foo.bar.com'; - const connector = new JsonRpcConnector(); - const spyOnCheckCompat = jest - .spyOn(connector, 'checkCompat') - .mockResolvedValue(true); - const { result } = renderHook(() => - useIsWalletServiceRunning(url, connector) - ); - - expect(result.current).toBe(null); - - await waitFor(() => { - expect(spyOnCheckCompat).toHaveBeenCalled(); - expect(result.current).toBe(true); - }); - }); - - it('returns false if wallet is not running', async () => { - const url = 'https://foo.bar.com'; - const connector = new JsonRpcConnector(); - const spyOnCheckCompat = jest - .spyOn(connector, 'checkCompat') - .mockRejectedValue(false); - const { result } = renderHook(() => - useIsWalletServiceRunning(url, connector) - ); - - expect(result.current).toBe(null); - - await waitFor(() => { - expect(spyOnCheckCompat).toHaveBeenCalled(); - expect(result.current).toBe(false); - }); - }); -}); diff --git a/libs/wallet/src/use-is-wallet-service-running.ts b/libs/wallet/src/use-is-wallet-service-running.ts deleted file mode 100644 index 5c03f38be..000000000 --- a/libs/wallet/src/use-is-wallet-service-running.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { JsonRpcConnector } from './connectors'; - -export const useIsWalletServiceRunning = ( - url: string, - connector: JsonRpcConnector | undefined -) => { - const [isRunning, setIsRunning] = useState(null); - - useEffect(() => { - if (!connector) return; - - if (url && url !== connector.url) { - connector.url = url; - } - - const check = async () => { - try { - // we are not checking wallet compatibility here, only that the wallet is running - await connector.checkCompat(); - setIsRunning(true); - } catch { - setIsRunning(false); - } - }; - - // check immediately - check(); - - // check every second for quick feedback to the user - const interval = setInterval(check, 1000); - - return () => { - clearInterval(interval); - }; - }, [connector, url]); - - return isRunning; -}; diff --git a/libs/wallet/src/use-json-rpc-connect.ts b/libs/wallet/src/use-json-rpc-connect.ts deleted file mode 100644 index 88fc5906d..000000000 --- a/libs/wallet/src/use-json-rpc-connect.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useCallback, useState } from 'react'; -import { WalletClientError } from '@vegaprotocol/wallet-client'; -import type { JsonRpcConnector } from './connectors'; -import { ClientErrors } from './connectors'; -import { useVegaWallet } from './use-vega-wallet'; -import { isTestEnv } from '@vegaprotocol/utils'; - -export enum Status { - Idle = 'Idle', - CheckingVersion = 'CheckingVersion', - GettingChainId = 'GettingChainId', - Connecting = 'Connecting', - GettingPerms = 'GettingPerms', - Connected = 'Connected', - Error = 'Error', - AcknowledgeNeeded = 'AcknowledgeNeeded', -} - -export const useJsonRpcConnect = (onConnect: () => void) => { - const { connect, acknowledgeNeeded } = useVegaWallet(); - const [status, setStatus] = useState(Status.Idle); - const [error, setError] = useState(null); - - const attemptConnect = useCallback( - async (connector: JsonRpcConnector, appChainId: string) => { - try { - // Check that the running wallet is compatible with this connector - setStatus(Status.CheckingVersion); - await connector.checkCompat(); - - // Check if wallet is configured for the same chain as the app - setStatus(Status.GettingChainId); - - // Do not throw in when cypress is running as trading app relies on - // mocks which result in a mismatch between chainId for app and - // chainId for wallet - if (!isTestEnv()) { - const chainIdResult = await connector.getChainId(); - if (chainIdResult.chainID !== appChainId) { - // Throw wallet error for consistent error handling - throw ClientErrors.WRONG_NETWORK; - } - } - - // Start connection flow. User will be prompted to select a wallet and enter - // its password in the wallet application, promise will resolve once successful - // or it will throw - setStatus(Status.Connecting); - await connector.connectWallet(); - - setStatus(Status.GettingPerms); - - // Call connect in the wallet provider. The connector will be stored for - // future actions such as sending transactions - await connect(connector); - if (acknowledgeNeeded) { - setStatus(Status.AcknowledgeNeeded); - } else { - setStatus(Status.Connected); - onConnect(); - } - } catch (err) { - if (err instanceof WalletClientError) { - setError(err); - } - setStatus(Status.Error); - } - }, - [onConnect, connect, acknowledgeNeeded] - ); - - return { - status, - error, - connect: attemptConnect, - }; -}; diff --git a/libs/wallet/src/use-snap-status.ts b/libs/wallet/src/use-snap-status.ts deleted file mode 100644 index 87c267aa9..000000000 --- a/libs/wallet/src/use-snap-status.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getSnap } from './connectors'; - -const INTERVAL = 2_000; - -export enum SnapStatus { - NOT_SUPPORTED, - INSTALLED, - NOT_INSTALLED, -} - -export const useSnapStatus = (snapId: string, shouldCheck: boolean) => { - const [status, setStatus] = useState(SnapStatus.NOT_INSTALLED); - useEffect(() => { - if (!shouldCheck) return; - - const checkState = async () => { - try { - const snap = await getSnap(snapId); - setStatus(snap ? SnapStatus.INSTALLED : SnapStatus.NOT_INSTALLED); - } catch (err) { - setStatus(SnapStatus.NOT_SUPPORTED); - } - }; - - const i = setInterval(() => { - checkState(); - }, INTERVAL); - - checkState(); - - return () => { - clearInterval(i); - }; - }, [snapId, shouldCheck]); - return status; -}; diff --git a/libs/wallet/src/use-vega-wallet.ts b/libs/wallet/src/use-vega-wallet.ts deleted file mode 100644 index f06f82453..000000000 --- a/libs/wallet/src/use-vega-wallet.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useCallback, useContext } from 'react'; -import { useVegaWalletDialogStore } from './connect-dialog/vega-wallet-dialog-store'; -import { VegaWalletContext } from './context'; - -export function useVegaWallet() { - const context = useContext(VegaWalletContext); - if (context === undefined) { - throw new Error('useVegaWallet must be used within VegaWalletProvider'); - } - return context; -} - -export function useReconnectVegaWallet() { - const openVegaWalletDialog = useVegaWalletDialogStore( - (store) => store.openVegaWalletDialog - ); - const { disconnect } = useVegaWallet(); - const reconnect = useCallback(async () => { - await disconnect(); - openVegaWalletDialog(); - }, [disconnect, openVegaWalletDialog]); - - return reconnect; -} diff --git a/libs/wallet/src/utils.spec.ts b/libs/wallet/src/utils.spec.ts index 22d22f659..898dc4436 100644 --- a/libs/wallet/src/utils.spec.ts +++ b/libs/wallet/src/utils.spec.ts @@ -1,5 +1,6 @@ import { determineId, normalizeOrderAmendment } from './utils'; import * as Schema from '@vegaprotocol/types'; + describe('determineId', () => { it('produces a known result for an ID', () => { const res = determineId( diff --git a/libs/wallet/src/utils.ts b/libs/wallet/src/utils.ts index 8c0c20c64..24bb26067 100644 --- a/libs/wallet/src/utils.ts +++ b/libs/wallet/src/utils.ts @@ -1,11 +1,25 @@ import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils'; -import type { Market, Order } from '@vegaprotocol/types'; -import type { AccountType } from '@vegaprotocol/types'; +import { type AccountType, type Market, type Order } from '@vegaprotocol/types'; import BigNumber from 'bignumber.js'; import { ethers } from 'ethers'; import { sha3_256 } from 'js-sha3'; -import type { OrderAmendment, Transaction, Transfer } from './connectors'; import type { Exact } from 'type-fest'; +import { + type ApplyReferralCode, + type BatchMarketInstructionSubmissionBody, + type CreateReferralSet, + type OrderAmendmentBody, + type OrderCancellationBody, + type OrderSubmissionBody, + type StopOrdersCancellationBody, + type StopOrdersSubmissionBody, + type TransferBody, + type UpdateMarginModeBody, + type WithdrawSubmissionBody, + type Transfer, + type OrderAmendment, + type Transaction, +} from './transaction-types'; /** * Creates an ID in the same way that core does on the backend. This way we @@ -66,4 +80,60 @@ export const normalizeTransfer = >( }; }; +/** + * TODO: We may want to create a package similar to @metamask/detect-ethereum-provider as this wont suffice + * if called immeidately, and before the extension has been able to add the vega object to the window + */ export const isBrowserWalletInstalled = () => Boolean(window.vega); + +/** + * TODO: We may want to use @metamask/detect-ethereum-provider here. It works for now as this is called + * when the dialog is opened so metamask has had plenty of time to add the ethereum object to the window + */ +export const isMetaMaskInstalled = () => { + return window.ethereum && window.ethereum.isMetaMask; +}; + +export const isMarginModeUpdateTransaction = ( + transaction: Transaction +): transaction is UpdateMarginModeBody => 'updateMarginMode' in transaction; + +export const isWithdrawTransaction = ( + transaction: Transaction +): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction; + +export const isOrderSubmissionTransaction = ( + transaction: Transaction +): transaction is OrderSubmissionBody => 'orderSubmission' in transaction; + +export const isOrderCancellationTransaction = ( + transaction: Transaction +): transaction is OrderCancellationBody => 'orderCancellation' in transaction; + +export const isStopOrdersSubmissionTransaction = ( + transaction: Transaction +): transaction is StopOrdersSubmissionBody => + 'stopOrdersSubmission' in transaction; + +export const isStopOrdersCancellationTransaction = ( + transaction: Transaction +): transaction is StopOrdersCancellationBody => + 'stopOrdersCancellation' in transaction; + +export const isOrderAmendmentTransaction = ( + transaction: Transaction +): transaction is OrderAmendmentBody => 'orderAmendment' in transaction; + +export const isBatchMarketInstructionsTransaction = ( + transaction: Transaction +): transaction is BatchMarketInstructionSubmissionBody => + 'batchMarketInstructions' in transaction; + +export const isTransferTransaction = ( + transaction: Transaction +): transaction is TransferBody => 'transfer' in transaction; + +export const isReferralRelatedTransaction = ( + transaction: Transaction +): transaction is CreateReferralSet | ApplyReferralCode => + 'createReferralSet' in transaction || 'applyReferralCode' in transaction; diff --git a/libs/wallet/src/wallet.spec.ts b/libs/wallet/src/wallet.spec.ts new file mode 100644 index 000000000..8b59a7153 --- /dev/null +++ b/libs/wallet/src/wallet.spec.ts @@ -0,0 +1,222 @@ +import { mockChain } from './chains'; +import { MockConnector, mockKeys } from './connectors'; +import { noConnectorError, userRejectedError } from './errors'; +import { createConfig, STORE_KEY } from './wallet'; + +beforeEach(() => { + localStorage.removeItem(STORE_KEY); +}); + +describe('chainId', () => { + const mockConnector = new MockConnector(); + + it('uses the default chainId', () => { + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + expect(config.store.getState().chainId).toEqual(mockChain.id); + }); + + it('handles invalid configuration', () => { + expect(() => { + createConfig({ + chains: [mockChain], + defaultChainId: 'invalid chain id', + connectors: [mockConnector], + }); + }).toThrow(); + }); +}); + +describe('connect', () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + it('handles invalid connector', async () => { + // @ts-expect-error deliberate wrong connector type + const result = await config.connect('invalid connector'); + expect(result).toEqual({ status: 'disconnected' }); + expect(config.store.getState()).toMatchObject({ + status: 'disconnected', + error: noConnectorError(), + current: undefined, + keys: [], + pubKey: undefined, + }); + }); + + it('can connect', async () => { + const spyConnect = jest.spyOn(mockConnector, 'connectWallet'); + const spyOn = jest.spyOn(mockConnector, 'on'); + const spyOff = jest.spyOn(mockConnector, 'off'); + + const result = await config.connect('mock'); + + expect(config.store.getState()).toMatchObject({ + status: 'connected', + keys: mockKeys, + pubKey: mockKeys[0].publicKey, + }); + + expect(spyConnect).toHaveBeenCalledTimes(1); + expect(spyConnect).toHaveBeenCalledWith(mockChain.id); + expect(spyOff).toHaveBeenCalledWith( + 'client.disconnected', + expect.any(Function) + ); + expect(spyOn).toHaveBeenCalledWith( + 'client.disconnected', + expect.any(Function) + ); + + expect(result).toEqual({ status: 'connected' }); + }); +}); + +describe('disconnect', () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + it('handles invalid connector', async () => { + const result = await config.disconnect(); + expect(result).toEqual({ status: 'disconnected' }); + expect(config.store.getState()).toMatchObject({ + status: 'disconnected', + error: noConnectorError(), + current: undefined, + keys: [], + pubKey: undefined, + }); + }); + + it('can disconnect', async () => { + const spyOff = jest.spyOn(mockConnector, 'off'); + const spyDisconnect = jest.spyOn(mockConnector, 'disconnectWallet'); + + await config.connect('mock'); + expect(config.store.getState().status).toBe('connected'); + const result = await config.disconnect(); + expect(spyOff).toHaveBeenCalledWith('client.disconnected'); + expect(spyDisconnect).toHaveBeenCalledTimes(1); + expect(config.store.getState()).toMatchObject({ + status: 'disconnected', + keys: [], + pubKey: undefined, + current: undefined, + }); + expect(result).toEqual({ status: 'disconnected' }); + }); +}); + +describe('refresh keys', () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + it('handles invalid connector', async () => { + await config.refreshKeys(); + expect(config.store.getState()).toMatchObject({ + error: noConnectorError(), + }); + }); + + it('can refresh to get newly created keys', async () => { + await config.connect('mock'); + expect(config.store.getState().status).toBe('connected'); + + const keys = [...mockKeys, { name: 'New key', publicKey: '123' }]; + jest.spyOn(mockConnector, 'listKeys').mockResolvedValue(keys); + + await config.refreshKeys(); + expect(config.store.getState().keys).toHaveLength(keys.length); + expect(config.store.getState()).toMatchObject({ + keys: keys, + }); + }); +}); + +describe('sendTransaction', () => { + const params = { + sendingMode: 'TYPE_SYNC', + publicKey: '123', + transaction: { + joinTeam: { + id: '123', + }, + }, + } as const; + + it('handles sending before connected', async () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + await expect(config.sendTransaction(params)).rejects.toEqual( + noConnectorError() + ); + }); + + it('handles connector error', async () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + jest + .spyOn(mockConnector, 'sendTransaction') + .mockRejectedValue(userRejectedError()); + + await config.connect('mock'); + + expect(config.store.getState().status).toBe('connected'); + + await expect(config.sendTransaction(params)).rejects.toEqual( + userRejectedError() + ); + }); + + it('can send transaction', async () => { + const mockConnector = new MockConnector(); + const config = createConfig({ + chains: [mockChain], + defaultChainId: mockChain.id, + connectors: [mockConnector], + }); + + const spySendTx = jest.spyOn(mockConnector, 'sendTransaction'); + + await config.connect('mock'); + + expect(config.store.getState().status).toBe('connected'); + + const tx = await config.sendTransaction(params); + + expect(tx).toMatchObject({ + transactionHash: expect.any(String), + signature: expect.any(String), + sentAt: expect.any(String), + receivedAt: expect.any(String), + }); + expect(spySendTx).toHaveBeenCalledTimes(1); + expect(spySendTx).toHaveBeenCalledWith(params); + }); +}); diff --git a/libs/wallet/src/wallet.ts b/libs/wallet/src/wallet.ts new file mode 100644 index 000000000..c18005a6e --- /dev/null +++ b/libs/wallet/src/wallet.ts @@ -0,0 +1,196 @@ +import { createStore } from 'zustand/vanilla'; +import { persist } from 'zustand/middleware'; +import { + type Wallet, + type Config, + type Store, + type TransactionParams, + type SingleKeyStore, + type CoreStore, + type Connector, + type ConnectorType, +} from './types'; +import { ConnectorError, noConnectorError, unknownError } from './errors'; + +export const STORE_KEY = 'vega_wallet_store'; + +// get/set functions are not used in the slices so these +// can be plain objects +export const singleKeyStoreSlice: SingleKeyStore = { + pubKey: undefined, +}; + +// get/set functions are not used in the slices so these +// can be plain objects +export const coreStoreSlice: CoreStore = { + chainId: '', + status: 'disconnected', + current: undefined, + keys: [], + error: undefined, + jsonRpcToken: undefined, +}; + +export function createConfig(cfg: Config): Wallet { + const chain = cfg.chains.find((c) => c.id === cfg.defaultChainId); + + if (!chain) { + throw new Error('default chain not found in config'); + } + + const getInitialState = () => { + return { + ...coreStoreSlice, + ...singleKeyStoreSlice, + chainId: chain.id, + }; + }; + + const store = createStore()( + persist(getInitialState, { + name: STORE_KEY, + partialize(state) { + return { + chainId: state.chainId, + current: state.current, + pubKey: state.pubKey, + jsonRpcToken: state.jsonRpcToken, + }; + }, + }) + ); + + const connectors = createStore(() => cfg.connectors.map(bindStore)); + + function bindStore(connector: Connector) { + connector.bindStore(store); + return connector; + } + + async function connect(id: ConnectorType) { + if (store.getState().status === 'connecting') { + return { status: 'connecting' as const }; + } + + try { + const connector = connectors.getState().find((c) => c.id === id); + + if (!connector) { + throw noConnectorError(); + } + + store.setState({ status: 'connecting', current: id, error: undefined }); + + await connector.connectWallet(store.getState().chainId); + const keys = await connector.listKeys(); + + // TODO: this shouldnt be in the default config as we dont want to enforce single key usage + // need to find a way to optin into using this slice in the store + const storedPubKey = store.getState().pubKey; + let defaultKey; + if (keys.find((k) => k.publicKey === storedPubKey)) { + defaultKey = storedPubKey; + } else { + defaultKey = keys[0].publicKey; + } + + store.setState({ + keys, + status: 'connected', + pubKey: defaultKey, + }); + + connector.off('client.disconnected', disconnect); + connector.on('client.disconnected', disconnect); + + return { status: 'connected' as const }; + } catch (err) { + store.setState({ + status: 'disconnected', + current: undefined, + keys: [], + error: err instanceof ConnectorError ? err : unknownError(), + }); + return { status: 'disconnected' as const }; + } + } + + async function disconnect() { + const connector = connectors + .getState() + .find((x) => x.id === store.getState().current); + + try { + if (!connector) { + throw noConnectorError(); + } + + connector.off('client.disconnected'); + + await connector.disconnectWallet(); + + store.setState(getInitialState(), true); + return { status: 'disconnected' as const }; + } catch (err) { + store.setState({ + ...getInitialState(), + error: err instanceof ConnectorError ? err : unknownError(), + }); + return { status: 'disconnected' as const }; + } + } + + async function refreshKeys() { + const connector = connectors + .getState() + .find((x) => x.id === store.getState().current); + + try { + if (!connector) { + throw noConnectorError(); + } + + const keys = await connector.listKeys(); + store.setState({ keys }); + } catch (err) { + store.setState({ + error: err instanceof ConnectorError ? err : unknownError(), + }); + } + } + + async function sendTransaction(params: TransactionParams) { + const connector = connectors + .getState() + .find((x) => x.id === store.getState().current); + + try { + if (!connector) { + throw noConnectorError(); + } + return connector.sendTransaction(params); + } catch (err) { + if (err instanceof ConnectorError) { + throw err; + } + throw unknownError(); + } + } + + function reset() { + store.setState(getInitialState(), true); + } + + return { + store, + connect, + disconnect, + refreshKeys, + sendTransaction, + reset, + + get connectors() { + return connectors.getState(); + }, + }; +} diff --git a/libs/web3/src/lib/use-ethereum-transaction-updater.spec.tsx b/libs/web3/src/lib/use-ethereum-transaction-updater.spec.tsx index 1d18ce135..850ff2962 100644 --- a/libs/web3/src/lib/use-ethereum-transaction-updater.spec.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction-updater.spec.tsx @@ -4,23 +4,23 @@ import { MockedProvider } from '@apollo/client/testing'; import { type ReactNode } from 'react'; import { useEthTransactionUpdater } from './use-ethereum-transaction-updater'; import { DepositBusEventDocument } from './__generated__/TransactionResult'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; import { type DepositBusEventSubscription, type DepositBusEventFieldsFragment, } from './__generated__/TransactionResult'; -import { type VegaWalletContextShape } from '@vegaprotocol/wallet'; import { type EthTransactionStore } from './use-ethereum-transaction-store'; import { DepositStatus } from '@vegaprotocol/types'; +import { + MockedWalletProvider, + mockConfig, +} from '@vegaprotocol/wallet-react/testing'; const pubKey = 'pubKey'; const render = (mocks?: MockedResponse[]) => { const wrapper = ({ children }: { children: ReactNode }) => ( - - {children} - + {children} ); return renderHook(() => useEthTransactionUpdater(), { wrapper }); @@ -73,10 +73,12 @@ const mockedDepositBusEvent: MockedResponse = { describe('useEthTransactionUpdater', () => { it('updates deposit on DepositBusEvents', async () => { + mockConfig.store.setState({ pubKey }); mockTransactionStoreState.mockReturnValue(defaultState); render([mockedDepositBusEvent]); await waitFor(() => { expect(updateDeposit).toHaveBeenCalledWith(depositBusEvent); }); + mockConfig.reset(); }); }); diff --git a/libs/web3/src/lib/use-ethereum-transaction-updater.tsx b/libs/web3/src/lib/use-ethereum-transaction-updater.tsx index b680ddc57..4a2829a17 100644 --- a/libs/web3/src/lib/use-ethereum-transaction-updater.tsx +++ b/libs/web3/src/lib/use-ethereum-transaction-updater.tsx @@ -1,5 +1,5 @@ import { DepositStatus } from '@vegaprotocol/types'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useDepositBusEventSubscription } from './__generated__/TransactionResult'; import { useEthTransactionStore } from './use-ethereum-transaction-store'; diff --git a/libs/web3/src/lib/use-vega-transaction-manager.spec.tsx b/libs/web3/src/lib/use-vega-transaction-manager.spec.tsx index c68459554..a1346aeac 100644 --- a/libs/web3/src/lib/use-vega-transaction-manager.spec.tsx +++ b/libs/web3/src/lib/use-vega-transaction-manager.spec.tsx @@ -1,5 +1,5 @@ import { useVegaTransactionManager } from './use-vega-transaction-manager'; -import { renderHook } from '@testing-library/react'; +import { act, renderHook } from '@testing-library/react'; import waitForNextTick from 'flush-promises'; import { type TransactionResponse } from '@vegaprotocol/wallet'; import { VegaTxStatus } from './types'; @@ -7,20 +7,12 @@ import { type VegaTransactionStore, type VegaStoredTxState, } from './use-vega-transaction-store'; - -const mockSendTx = jest.fn(); - -const pubKey = 'pubKey'; +import { + mockConfig, + MockedWalletProvider, +} from '@vegaprotocol/wallet-react/testing'; const mockDisconnect = jest.fn(); -jest.mock('@vegaprotocol/wallet', () => ({ - ...jest.requireActual('@vegaprotocol/wallet'), - useVegaWallet: () => ({ - sendTx: mockSendTx, - pubKey, - disconnect: mockDisconnect, - }), -})); const transactionHash = 'txHash'; const signature = 'signature'; @@ -66,24 +58,39 @@ jest.mock('./use-vega-transaction-store', () => ({ describe('useVegaTransactionManager', () => { beforeEach(() => { + mockConfig.store.setState({ status: 'connected', pubKey: 'my-key' }); update.mockReset(); del.mockReset(); - mockSendTx.mockReset(); - mockTransactionStoreState.mockReset(); }); + afterEach(() => { + act(() => { + mockConfig.reset(); + }); + }); + + const setup = () => { + return renderHook(() => useVegaTransactionManager(), { + wrapper: MockedWalletProvider, + }); + }; + it('sendTx of first pending transaction', async () => { + jest + .spyOn(mockConfig, 'sendTransaction') + .mockResolvedValue(transactionResponse); mockTransactionStoreState.mockReturnValue(defaultState); - mockSendTx.mockResolvedValue(transactionResponse); - let result = renderHook(useVegaTransactionManager); + let result = setup(); result.rerender(); expect(update).not.toBeCalled(); + await waitForNextTick(); + expect(update.mock.calls[0]).toEqual([0, pendingTransactionUpdate]); expect(update.mock.calls[1]).toEqual([1, pendingTransactionUpdate]); update.mockReset(); - result = renderHook(useVegaTransactionManager); + result = setup(); await waitForNextTick(); expect(update).toBeCalled(); expect(update.mock.calls[0]).toEqual([0, pendingTransactionUpdate]); @@ -94,8 +101,9 @@ describe('useVegaTransactionManager', () => { it('del transaction on null response', async () => { mockTransactionStoreState.mockReturnValue(defaultState); - mockSendTx.mockResolvedValue(null); - renderHook(useVegaTransactionManager); + // @ts-ignore overriding resolved value + jest.spyOn(mockConfig, 'sendTransaction').mockResolvedValue(null); + setup(); await waitForNextTick(); expect(update).not.toBeCalled(); expect(del).toBeCalled(); @@ -103,8 +111,8 @@ describe('useVegaTransactionManager', () => { it('sets error on reject', async () => { mockTransactionStoreState.mockReturnValue(defaultState); - mockSendTx.mockRejectedValue(null); - renderHook(useVegaTransactionManager); + jest.spyOn(mockConfig, 'sendTransaction').mockRejectedValue(null); + setup(); await waitForNextTick(); expect(mockDisconnect).not.toHaveBeenCalledWith(); expect(update).toBeCalled(); @@ -113,10 +121,13 @@ describe('useVegaTransactionManager', () => { it('call disconnect if detect no service error', async () => { mockTransactionStoreState.mockReturnValue(defaultState); - mockSendTx.mockRejectedValue(new TypeError('Failed to fetch')); - renderHook(useVegaTransactionManager); + jest + .spyOn(mockConfig, 'sendTransaction') + .mockRejectedValue(new TypeError('Failed to fetch')); + setup(); await waitForNextTick(); - expect(mockDisconnect).toHaveBeenCalledWith(); + // TODO: fix disconnect on failure + // expect(mockDisconnect).toHaveBeenCalledWith(); expect(update).toBeCalled(); expect(update.mock.calls[0][1]?.status).toEqual(VegaTxStatus.Error); }); diff --git a/libs/web3/src/lib/use-vega-transaction-manager.tsx b/libs/web3/src/lib/use-vega-transaction-manager.tsx index 26e73c261..ff8be6973 100644 --- a/libs/web3/src/lib/use-vega-transaction-manager.tsx +++ b/libs/web3/src/lib/use-vega-transaction-manager.tsx @@ -1,27 +1,18 @@ -import { useVegaWallet, WalletError, ClientErrors } from '@vegaprotocol/wallet'; +import { + useDisconnect, + useSendTransaction, + useVegaWallet, +} from '@vegaprotocol/wallet-react'; +import { ConnectorError, ConnectorErrors } from '@vegaprotocol/wallet'; import { useEffect, useRef } from 'react'; import { useVegaTransactionStore } from './use-vega-transaction-store'; -import { - WalletClientError, - WalletHttpError, -} from '@vegaprotocol/wallet-client'; import { VegaTxStatus } from './types'; -const orderErrorResolve = (err: Error | unknown): Error => { - if (err instanceof WalletClientError || err instanceof WalletError) { - return err; - } else if (err instanceof WalletHttpError) { - return ClientErrors.UNKNOWN; - } else if (err instanceof TypeError) { - return ClientErrors.NO_SERVICE; - } else if (err instanceof Error) { - return err; - } - return ClientErrors.UNKNOWN; -}; - export const useVegaTransactionManager = () => { - const { sendTx, pubKey, disconnect } = useVegaWallet(); + const { pubKey } = useVegaWallet(); + const { disconnect } = useDisconnect(); + const { sendTransaction } = useSendTransaction(); + const processed = useRef>(new Set()); const transaction = useVegaTransactionStore((state) => state.transactions.find( @@ -37,13 +28,18 @@ export const useVegaTransactionManager = () => { return; } processed.current.add(transaction.id); - sendTx(pubKey, transaction.body) + + sendTransaction({ + publicKey: pubKey, + sendingMode: 'TYPE_SYNC', + transaction: transaction.body, + }) .then((res) => { - if (res === null) { - // User rejected + if (!res) { del(transaction.id); return; } + if (res.signature && res.transactionHash) { update(transaction.id, { status: VegaTxStatus.Pending, @@ -53,14 +49,20 @@ export const useVegaTransactionManager = () => { } }) .catch((err) => { - const error = orderErrorResolve(err); - if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) { + if ( + err instanceof ConnectorError && + err.code === ConnectorErrors.noWallet.code + ) { disconnect(); } + update(transaction.id, { - error, + error: + err instanceof ConnectorError + ? err + : new Error('something went wrong'), status: VegaTxStatus.Error, }); }); - }, [transaction, pubKey, del, sendTx, update, disconnect]); + }, [transaction, pubKey, del, sendTransaction, update, disconnect]); }; diff --git a/libs/web3/src/lib/use-vega-transaction-toasts.tsx b/libs/web3/src/lib/use-vega-transaction-toasts.tsx index 94a5b2a70..ac5882991 100644 --- a/libs/web3/src/lib/use-vega-transaction-toasts.tsx +++ b/libs/web3/src/lib/use-vega-transaction-toasts.tsx @@ -1,25 +1,9 @@ import { useCallback } from 'react'; import first from 'lodash/first'; import compact from 'lodash/compact'; -import type { - BatchMarketInstructionSubmissionBody, - OrderAmendment, - OrderSubmission, - StopOrdersSubmission, - StopOrderSetup, - UpdateMarginMode, -} from '@vegaprotocol/wallet'; -import type { - OrderTxUpdateFieldsFragment, - WithdrawalBusEventFieldsFragment, -} from './__generated__/TransactionResult'; -import type { VegaStoredTxState } from './use-vega-transaction-store'; import { isTransferTransaction, isBatchMarketInstructionsTransaction, - ClientErrors, - useReconnectVegaWallet, - WalletError, isOrderAmendmentTransaction, isOrderCancellationTransaction, isOrderSubmissionTransaction, @@ -29,7 +13,20 @@ import { isReferralRelatedTransaction, isMarginModeUpdateTransaction, MarginMode, + type BatchMarketInstructionSubmissionBody, + type OrderAmendment, + type OrderSubmission, + type StopOrdersSubmission, + type StopOrderSetup, + type UpdateMarginMode, + ConnectorErrors, + ConnectorError, } from '@vegaprotocol/wallet'; +import type { + OrderTxUpdateFieldsFragment, + WithdrawalBusEventFieldsFragment, +} from './__generated__/TransactionResult'; +import type { VegaStoredTxState } from './use-vega-transaction-store'; import { useVegaTransactionStore } from './use-vega-transaction-store'; import { VegaTxStatus } from './types'; import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit'; @@ -62,6 +59,7 @@ import { useWithdrawalApprovalDialog } from './withdrawal-approval-dialog'; import * as Schema from '@vegaprotocol/types'; import { Trans } from 'react-i18next'; import { useT } from './use-t'; +import { useReconnect } from '@vegaprotocol/wallet-react'; export const getRejectionReason = ( order: OrderTxUpdateFieldsFragment, @@ -903,20 +901,19 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => { const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => { const t = useT(); let label = t('Error occurred'); - let errorMessage = - tx.error instanceof WalletError - ? `${tx.error.title}: ${tx.error.data}` - : tx.error?.message; + let errorMessage = tx.error?.message; - const reconnectVegaWallet = useReconnectVegaWallet(); + const reconnectVegaWallet = useReconnect(); const orderRejection = tx.order && getRejectionReason(tx.order, t); + const walletNoConnectionCodes = [ - ClientErrors.NO_SERVICE.code, - ClientErrors.NO_CLIENT.code, + ConnectorErrors.noWallet.code, + ConnectorErrors.noConnector.code, ]; + const walletError = - tx.error instanceof WalletError && + tx.error instanceof ConnectorError && walletNoConnectionCodes.includes(tx.error.code); if (orderRejection) { @@ -928,6 +925,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => { } ); } + if (walletError) { label = t('Wallet disconnected'); errorMessage = t('The connection to your Vega Wallet has been lost.'); diff --git a/libs/web3/src/lib/use-vega-transaction-updater.spec.tsx b/libs/web3/src/lib/use-vega-transaction-updater.spec.tsx index 5965ecddc..fb5c9df79 100644 --- a/libs/web3/src/lib/use-vega-transaction-updater.spec.tsx +++ b/libs/web3/src/lib/use-vega-transaction-updater.spec.tsx @@ -36,7 +36,7 @@ const render = (mocks?: MockedResponse[]) => { const pubKey = 'pubKey'; -jest.mock('@vegaprotocol/wallet', () => ({ +jest.mock('@vegaprotocol/wallet-react', () => ({ useVegaWallet: () => ({ pubKey, }), diff --git a/libs/web3/src/lib/use-vega-transaction-updater.tsx b/libs/web3/src/lib/use-vega-transaction-updater.tsx index d7d40d44f..6319317f0 100644 --- a/libs/web3/src/lib/use-vega-transaction-updater.tsx +++ b/libs/web3/src/lib/use-vega-transaction-updater.tsx @@ -1,5 +1,5 @@ import { useApolloClient } from '@apollo/client'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { useOrderTxUpdateSubscription, useWithdrawalBusEventSubscription, diff --git a/libs/web3/src/lib/use-wallet-disconnected-toasts.tsx b/libs/web3/src/lib/use-wallet-disconnected-toasts.tsx index 462f7a0a3..000ff1ef4 100644 --- a/libs/web3/src/lib/use-wallet-disconnected-toasts.tsx +++ b/libs/web3/src/lib/use-wallet-disconnected-toasts.tsx @@ -5,10 +5,9 @@ import { CLOSE_AFTER, type Toast, } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useConnector } from '@vegaprotocol/wallet-react'; import { useEffect, useMemo } from 'react'; import { useT } from './use-t'; -import { usePrevious } from '@vegaprotocol/react-helpers'; export const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID'; @@ -41,12 +40,9 @@ export const useWalletDisconnectedToasts = ( state.setToast, state.update, ]); + const { connector } = useConnector(); const { showToast, hideToast } = useWalletDisconnectToastActions(); - const { isAlive } = useVegaWallet(); - const wasAlive = usePrevious(isAlive); - const disconnected = wasAlive && !isAlive; - const toast: Toast = useMemo( () => ({ id: WALLET_DISCONNECTED_TOAST_ID, @@ -67,12 +63,20 @@ export const useWalletDisconnectedToasts = ( ); useEffect(() => { - if (disconnected) { + if (!connector) return; + + function handleDisconnect() { if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) { showToast(); } else { setToast(toast); } } - }, [disconnected, hasToast, isAlive, setToast, showToast, t, toast]); + + connector.on('client.disconnected', handleDisconnect); + + return () => { + connector.off('client.disconnected', handleDisconnect); + }; + }, [connector, hasToast, setToast, showToast, t, toast]); }; diff --git a/libs/withdraws/src/lib/create-withdrawal-dialog.tsx b/libs/withdraws/src/lib/create-withdrawal-dialog.tsx index d78b8588c..9163c98b4 100644 --- a/libs/withdraws/src/lib/create-withdrawal-dialog.tsx +++ b/libs/withdraws/src/lib/create-withdrawal-dialog.tsx @@ -1,5 +1,5 @@ import { Dialog } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import { WithdrawFormContainer } from './withdraw-form-container'; import { useWeb3ConnectStore } from '@vegaprotocol/web3'; import { useWithdrawalDialog } from './withdrawal-dialog'; diff --git a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.spec.ts b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.spec.ts index 35cde61d0..37d57176c 100644 --- a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.spec.ts +++ b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.spec.ts @@ -5,10 +5,10 @@ import * as web3 from '@vegaprotocol/web3'; import { renderHook, waitFor } from '@testing-library/react'; import { useIncompleteWithdrawals } from './use-ready-to-complete-withdrawals-toast'; import { MockedProvider } from '@apollo/client/testing'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; jest.mock('@vegaprotocol/web3'); -jest.mock('@vegaprotocol/wallet'); +jest.mock('@vegaprotocol/wallet-react'); type Asset = WithdrawalFieldsFragment['asset']; type Withdrawal = WithdrawalFieldsFragment; diff --git a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx index f7dbbefc5..458f27898 100644 --- a/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx +++ b/libs/withdraws/src/lib/use-ready-to-complete-withdrawals-toast.tsx @@ -1,5 +1,5 @@ import { useDataProvider } from '@vegaprotocol/data-provider'; -import { useVegaWallet } from '@vegaprotocol/wallet'; +import { useVegaWallet } from '@vegaprotocol/wallet-react'; import BigNumber from 'bignumber.js'; import type { Toast } from '@vegaprotocol/ui-toolkit'; import { Button, Intent, Panel, ToastHeading } from '@vegaprotocol/ui-toolkit'; diff --git a/package.json b/package.json index fc25476c8..1a1cfa7e8 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@sentry/nextjs": "^6.19.3", "@sentry/react": "^6.19.2", "@sentry/tracing": "^6.19.2", - "@vegaprotocol/wallet-client": "0.1.9", "@walletconnect/ethereum-provider": "^2.6.0", "@web3-react/coinbase-wallet": "8.1.2-beta.0", "@web3-react/core": "^8.1.2-beta.0", @@ -64,6 +63,7 @@ "date-fns-tz": "^2.0.0", "duration-js": "^4.0.0", "ethers": "^5.6.0", + "eventemitter3": "^5.0.1", "graphql": "^15.7.2", "graphql-request": "^5.0.0", "graphql-ws": "^5.6.3", @@ -99,7 +99,7 @@ "uuid": "^8.3.2", "web-vitals": "^2.1.4", "zod": "^3.20.3", - "zustand": "^4.3.2" + "zustand": "^4.5.0" }, "devDependencies": { "@apollo/react-testing": "^4.0.0", diff --git a/tsconfig.base.json b/tsconfig.base.json index 762462106..ee5b3fff8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -56,6 +56,10 @@ "@vegaprotocol/ui-toolkit": ["libs/ui-toolkit/src/index.ts"], "@vegaprotocol/utils": ["libs/utils/src/index.ts"], "@vegaprotocol/wallet": ["libs/wallet/src/index.ts"], + "@vegaprotocol/wallet-react": ["libs/wallet-react/src/index.ts"], + "@vegaprotocol/wallet-react/testing": [ + "libs/wallet-react/src/testing.ts" + ], "@vegaprotocol/web3": ["libs/web3/src/index.ts"], "@vegaprotocol/withdraws": ["libs/withdraws/src/index.ts"] } diff --git a/yarn.lock b/yarn.lock index 1ad981bfd..f85c075be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7815,15 +7815,6 @@ "@typescript-eslint/types" "6.11.0" eslint-visitor-keys "^3.4.1" -"@vegaprotocol/wallet-client@0.1.9": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.9.tgz#8c6a71c8b2222b3de5d73cade8fc6db57e332de9" - integrity sha512-oacfJGT0zHM+1If4I/pgIWi7zzU/3uHy4+sjuFEh8pWI8bWBzCF+mHbOGA6iTM+5/5mhayMFc+YZ9GexH1sBnQ== - dependencies: - express "4.18.2" - nanoid "3.3.4" - node-fetch "2.6.7" - "@walletconnect/browser-utils@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/browser-utils/-/browser-utils-1.8.0.tgz#33c10e777aa6be86c713095b5206d63d32df0951" @@ -12594,7 +12585,7 @@ eventemitter3@4.0.7, eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -eventemitter3@^5.0.0: +eventemitter3@^5.0.0, eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== @@ -12670,7 +12661,7 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -express@4.18.2, express@^4.17.3: +express@^4.17.3: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -17118,11 +17109,6 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== - nanoid@^3.3.1, nanoid@^3.3.4, nanoid@^3.3.6: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -17214,13 +17200,6 @@ node-fetch-native@^1.2.0, node-fetch-native@^1.4.0: resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.1.tgz#5a336e55b4e1b1e72b9927da09fecd2b374c9be5" integrity sha512-NsXBU0UgBxo2rQLOeWNZqS3fvflWePMECr8CoSWoSTqCqGbVVsvl9vZu1HfQicYN0g5piV9Gh8RTEvo/uP752w== -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - node-fetch@^2.0.0: version "2.6.11" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" @@ -22730,10 +22709,10 @@ zustand@4.4.0: dependencies: use-sync-external-store "1.2.0" -zustand@^4.3.2: - version "4.4.6" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.6.tgz#03c78e3e2686c47095c93714c0c600b72a6512bd" - integrity sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg== +zustand@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.0.tgz#141354af56f91de378aa6c4b930032ab338f3ef0" + integrity sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A== dependencies: use-sync-external-store "1.2.0" From 2d821700bd1d8462d01708465febf4eb3bca795f Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Fri, 1 Mar 2024 09:26:29 -0500 Subject: [PATCH 18/21] fix(trading): check for batch proposal in trading mode tooltip (#5895) --- .../trading-mode-tooltip.tsx | 39 ++++++++++--------- .../src/lib/proposals-hooks/Proposal.graphql | 13 +++++++ .../proposals-hooks/__generated__/Proposal.ts | 15 ++++++- libs/types/src/__generated__/types.ts | 2 + 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/libs/deal-ticket/src/components/trading-mode-tooltip/trading-mode-tooltip.tsx b/libs/deal-ticket/src/components/trading-mode-tooltip/trading-mode-tooltip.tsx index 56b6421bb..045bec5ef 100644 --- a/libs/deal-ticket/src/components/trading-mode-tooltip/trading-mode-tooltip.tsx +++ b/libs/deal-ticket/src/components/trading-mode-tooltip/trading-mode-tooltip.tsx @@ -1,11 +1,7 @@ import { useMemo } from 'react'; import { parseISO, isValid, isAfter } from 'date-fns'; import classNames from 'classnames'; -import { - useProposalOfMarketQuery, - type ProposalOfMarketQuery, - type SingleProposal, -} from '@vegaprotocol/proposals'; +import { useProposalOfMarketQuery } from '@vegaprotocol/proposals'; import { DocsLinks } from '@vegaprotocol/environment'; import { getDateTimeFormat } from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; @@ -40,15 +36,21 @@ export const TradingModeTooltip = ({ marketTradingMode, }); - // We only fetch Proposals (and not BatchProposals) - const proposal = proposalData?.proposal as SingleProposal< - ProposalOfMarketQuery['proposal'] - >; - if (!market || !marketData) { return null; } - const enactmentDate = parseISO(proposal?.terms.enactmentDatetime); + + let enactmentDate; + const proposal = proposalData?.proposal; + + if (proposal?.__typename === 'Proposal') { + enactmentDate = parseISO(proposal.terms.enactmentDatetime); + } else if (proposal?.__typename === 'BatchProposal') { + const change = proposal.batchTerms?.changes.find( + (c) => c?.change.__typename === 'NewMarket' + ); + enactmentDate = change ? parseISO(change.enactmentDatetime) : undefined; + } const compiledGrid = !skipGrid && compileGridData(t, market, marketData, onSelect); @@ -67,14 +69,16 @@ export const TradingModeTooltip = ({ return (

    - {isValid(enactmentDate) && isAfter(new Date(), enactmentDate) ? ( + {enactmentDate && + isValid(enactmentDate) && + isAfter(new Date(), enactmentDate) ? ( <> {`${Schema.MarketTradingModeMapping[marketTradingMode]}: ${t( @@ -91,7 +95,7 @@ export const TradingModeTooltip = ({ <> {isValid(enactmentDate) && ( {`${ @@ -109,10 +113,7 @@ export const TradingModeTooltip = ({ )} {DocsLinks && ( - + {t('Find out more')} )} diff --git a/libs/proposals/src/lib/proposals-hooks/Proposal.graphql b/libs/proposals/src/lib/proposals-hooks/Proposal.graphql index 009fa78dd..e9165a980 100644 --- a/libs/proposals/src/lib/proposals-hooks/Proposal.graphql +++ b/libs/proposals/src/lib/proposals-hooks/Proposal.graphql @@ -45,6 +45,19 @@ query ProposalOfMarket($marketId: ID!) { enactmentDatetime } } + ... on BatchProposal { + id + batchTerms { + changes { + enactmentDatetime + change { + ... on NewMarket { + __typename + } + } + } + } + } } } diff --git a/libs/proposals/src/lib/proposals-hooks/__generated__/Proposal.ts b/libs/proposals/src/lib/proposals-hooks/__generated__/Proposal.ts index d4e1a5cd4..83a090536 100644 --- a/libs/proposals/src/lib/proposals-hooks/__generated__/Proposal.ts +++ b/libs/proposals/src/lib/proposals-hooks/__generated__/Proposal.ts @@ -25,7 +25,7 @@ export type ProposalOfMarketQueryVariables = Types.Exact<{ }>; -export type ProposalOfMarketQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null } } | null }; +export type ProposalOfMarketQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, batchTerms?: { __typename?: 'BatchProposalTerms', changes: Array<{ __typename?: 'BatchProposalTermsChange', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } | null> } | null } | { __typename?: 'Proposal', id?: string | null, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null } } | null }; export type SuccessorMarketProposalDetailsQueryVariables = Types.Exact<{ proposalId: Types.Scalars['ID']; @@ -152,6 +152,19 @@ export const ProposalOfMarketDocument = gql` enactmentDatetime } } + ... on BatchProposal { + id + batchTerms { + changes { + enactmentDatetime + change { + ... on NewMarket { + __typename + } + } + } + } + } } } `; diff --git a/libs/types/src/__generated__/types.ts b/libs/types/src/__generated__/types.ts index ec9c650ef..809d35f86 100644 --- a/libs/types/src/__generated__/types.ts +++ b/libs/types/src/__generated__/types.ts @@ -256,6 +256,8 @@ export type AggregatedLedgerEntry = { toAccountPartyId?: Maybe; /** Account type, if query was grouped by receiver account type - else null */ toAccountType?: Maybe; + /** Transfer ID associated with this aggregated ledger entry */ + transferId: Scalars['ID']; /** Type of the transfer for this ledger entry */ transferType?: Maybe; /** RFC3339Nano time from at which this ledger entries records were relevant */ From 89e2033556b90cd3dacb6f191a37d3e4dba067ba Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Fri, 1 Mar 2024 09:26:43 -0500 Subject: [PATCH 19/21] chore(ci): remove unused release script (#5874) --- package.json | 1 - scripts/make-release.js | 82 ----------------------------------------- 2 files changed, 83 deletions(-) delete mode 100644 scripts/make-release.js diff --git a/package.json b/package.json index 1a1cfa7e8..af5c7b41c 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,6 @@ "flush-promises": "^1.0.2", "glob": "^8.0.3", "husky": "^7.0.4", - "inquirer": "^8.0.0", "jest": "29.4.3", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.4.1", diff --git a/scripts/make-release.js b/scripts/make-release.js deleted file mode 100644 index 6b6a03f37..000000000 --- a/scripts/make-release.js +++ /dev/null @@ -1,82 +0,0 @@ -const { execSync } = require('child_process'); -const inquirer = require('inquirer'); - -const getTags = (take) => { - const tags = execSync( - "git for-each-ref --sort=taggerdate --format '%(refname)' refs/tags" - ) - .toString() - .trim() - .split('\n') - .map((t) => t.replace('refs/tags/', '')) - .reverse() - .slice(0, take); - return tags; -}; - -const getReleaseBranches = () => { - const branches = execSync('git branch --remote | grep origin/release/') - .toString() - .trim() - .split('\n') - .map((b) => b.trim().replace('origin/release/', '')); - return branches; -}; - -const release = (tag, branch) => { - const steps = [ - `git checkout ${branch}`, - 'git pull', - `git reset --hard ${tag}`, - 'git push --force', - ]; - try { - for (const cmd of steps) { - const result = execSync(cmd).toString(); - } - } catch (err) { - console.error('Could not make a release'); - } -}; - -inquirer - .prompt([ - { - type: 'list', - name: 'tag', - message: 'What version would you like to release?', - choices: getTags(), - }, - { - type: 'checkbox', - name: 'envs', - message: 'To what environment you wish to release?', - choices: ['mainnet', ...getReleaseBranches()], - validate(answer) { - return answer.length > 0; - }, - }, - ]) - .then((answers) => { - inquirer - .prompt({ - type: 'confirm', - name: 'sure', - message: `Are you sure? This will release ${ - answers.tag - } to ${answers.envs.join(', ')}`, - }) - .then(() => { - for (const env of answers.envs) { - const branch = env === 'mainnet' ? 'main' : `release/${env}`; - release(answers.tag, branch); - } - execSync('git checkout develop'); - }) - .catch((err) => { - console.log('Something went wrong', err.toString()); - }); - }) - .catch((err) => { - console.log('Something went wrong', err.toString()); - }); From 00dbb7dd60296672983ab3006c116f90c0905c88 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Sun, 3 Mar 2024 09:25:04 -0500 Subject: [PATCH 20/21] chore(trading): delete trading-e2e cypress tests (#5901) --- .github/workflows/cypress-live-test.yml | 36 -- .github/workflows/cypress-manual-trigger.yml | 1 - .github/workflows/cypress-nightly.yml | 2 +- apps/trading-e2e/.env | 41 --- apps/trading-e2e/.env.capsule | 33 -- apps/trading-e2e/.eslintrc.json | 19 - apps/trading-e2e/cypress.config.js | 41 --- apps/trading-e2e/declaration.d.ts | 1 - apps/trading-e2e/project.json | 39 -- apps/trading-e2e/src/index.d.ts | 20 -- .../trading-e2e/src/integration/capsule.cy.ts | 337 ------------------ .../src/integration/wallet-eth.cy.ts | 43 --- apps/trading-e2e/src/support/create-order.ts | 29 -- apps/trading-e2e/src/support/deal-ticket.ts | 18 - .../src/support/ethereum-wallet.ts | 5 - apps/trading-e2e/src/support/helpers.ts | 11 - apps/trading-e2e/src/support/index.js | 8 - .../src/support/order-update-subscription.ts | 36 -- .../src/support/order-validation.ts | 81 ----- apps/trading-e2e/src/support/trading.ts | 252 ------------- apps/trading-e2e/tsconfig.json | 19 - apps/trading/README.md | 8 +- tools/ci/check-affected.py | 3 + 23 files changed, 5 insertions(+), 1078 deletions(-) delete mode 100644 .github/workflows/cypress-live-test.yml delete mode 100644 apps/trading-e2e/.env delete mode 100644 apps/trading-e2e/.env.capsule delete mode 100644 apps/trading-e2e/.eslintrc.json delete mode 100644 apps/trading-e2e/cypress.config.js delete mode 100644 apps/trading-e2e/declaration.d.ts delete mode 100644 apps/trading-e2e/project.json delete mode 100644 apps/trading-e2e/src/index.d.ts delete mode 100644 apps/trading-e2e/src/integration/capsule.cy.ts delete mode 100644 apps/trading-e2e/src/integration/wallet-eth.cy.ts delete mode 100644 apps/trading-e2e/src/support/create-order.ts delete mode 100644 apps/trading-e2e/src/support/deal-ticket.ts delete mode 100644 apps/trading-e2e/src/support/ethereum-wallet.ts delete mode 100644 apps/trading-e2e/src/support/helpers.ts delete mode 100644 apps/trading-e2e/src/support/index.js delete mode 100644 apps/trading-e2e/src/support/order-update-subscription.ts delete mode 100644 apps/trading-e2e/src/support/order-validation.ts delete mode 100644 apps/trading-e2e/src/support/trading.ts delete mode 100644 apps/trading-e2e/tsconfig.json diff --git a/.github/workflows/cypress-live-test.yml b/.github/workflows/cypress-live-test.yml deleted file mode 100644 index 4c39a6e28..000000000 --- a/.github/workflows/cypress-live-test.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/cypress-manual-trigger.yml b/.github/workflows/cypress-manual-trigger.yml index 090089c15..c1f46c011 100644 --- a/.github/workflows/cypress-manual-trigger.yml +++ b/.github/workflows/cypress-manual-trigger.yml @@ -12,7 +12,6 @@ on: options: - explorer-e2e - governance-e2e - - trading-e2e tags: description: 'Test tags to run' required: true diff --git a/.github/workflows/cypress-nightly.yml b/.github/workflows/cypress-nightly.yml index 16269f1b5..3ddbb0891 100644 --- a/.github/workflows/cypress-nightly.yml +++ b/.github/workflows/cypress-nightly.yml @@ -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' diff --git a/apps/trading-e2e/.env b/apps/trading-e2e/.env deleted file mode 100644 index 95b8d5c19..000000000 --- a/apps/trading-e2e/.env +++ /dev/null @@ -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 diff --git a/apps/trading-e2e/.env.capsule b/apps/trading-e2e/.env.capsule deleted file mode 100644 index cf2ef8201..000000000 --- a/apps/trading-e2e/.env.capsule +++ /dev/null @@ -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= diff --git a/apps/trading-e2e/.eslintrc.json b/apps/trading-e2e/.eslintrc.json deleted file mode 100644 index e680c3db5..000000000 --- a/apps/trading-e2e/.eslintrc.json +++ /dev/null @@ -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" - } - } - ] -} diff --git a/apps/trading-e2e/cypress.config.js b/apps/trading-e2e/cypress.config.js deleted file mode 100644 index b74c9d4fd..000000000 --- a/apps/trading-e2e/cypress.config.js +++ /dev/null @@ -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 }, - }, -}); diff --git a/apps/trading-e2e/declaration.d.ts b/apps/trading-e2e/declaration.d.ts deleted file mode 100644 index d5cf927a7..000000000 --- a/apps/trading-e2e/declaration.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.scss'; diff --git a/apps/trading-e2e/project.json b/apps/trading-e2e/project.json deleted file mode 100644 index 513754f96..000000000 --- a/apps/trading-e2e/project.json +++ /dev/null @@ -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"] -} diff --git a/apps/trading-e2e/src/index.d.ts b/apps/trading-e2e/src/index.d.ts deleted file mode 100644 index 0ef79cee8..000000000 --- a/apps/trading-e2e/src/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/// - -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; - } -} diff --git a/apps/trading-e2e/src/integration/capsule.cy.ts b/apps/trading-e2e/src/integration/capsule.cy.ts deleted file mode 100644 index 704ce2b1d..000000000 --- a/apps/trading-e2e/src/integration/capsule.cy.ts +++ /dev/null @@ -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})\)/ - ); - }); - }); - }); -}); diff --git a/apps/trading-e2e/src/integration/wallet-eth.cy.ts b/apps/trading-e2e/src/integration/wallet-eth.cy.ts deleted file mode 100644 index 9f10ce504..000000000 --- a/apps/trading-e2e/src/integration/wallet-eth.cy.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/apps/trading-e2e/src/support/create-order.ts b/apps/trading-e2e/src/support/create-order.ts deleted file mode 100644 index 17a7b0ede..000000000 --- a/apps/trading-e2e/src/support/create-order.ts +++ /dev/null @@ -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(); -}; diff --git a/apps/trading-e2e/src/support/deal-ticket.ts b/apps/trading-e2e/src/support/deal-ticket.ts deleted file mode 100644 index 02902226e..000000000 --- a/apps/trading-e2e/src/support/deal-ticket.ts +++ /dev/null @@ -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], - }; -}); diff --git a/apps/trading-e2e/src/support/ethereum-wallet.ts b/apps/trading-e2e/src/support/ethereum-wallet.ts deleted file mode 100644 index 16f5b970b..000000000 --- a/apps/trading-e2e/src/support/ethereum-wallet.ts +++ /dev/null @@ -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(); -}; diff --git a/apps/trading-e2e/src/support/helpers.ts b/apps/trading-e2e/src/support/helpers.ts deleted file mode 100644 index 85fa22a68..000000000 --- a/apps/trading-e2e/src/support/helpers.ts +++ /dev/null @@ -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); -}; diff --git a/apps/trading-e2e/src/support/index.js b/apps/trading-e2e/src/support/index.js deleted file mode 100644 index 06a4e4b14..000000000 --- a/apps/trading-e2e/src/support/index.js +++ /dev/null @@ -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(); diff --git a/apps/trading-e2e/src/support/order-update-subscription.ts b/apps/trading-e2e/src/support/order-update-subscription.ts deleted file mode 100644 index 3d9095324..000000000 --- a/apps/trading-e2e/src/support/order-update-subscription.ts +++ /dev/null @@ -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 -): 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)); -} diff --git a/apps/trading-e2e/src/support/order-validation.ts b/apps/trading-e2e/src/support/order-validation.ts deleted file mode 100644 index 8e2389433..000000000 --- a/apps/trading-e2e/src/support/order-validation.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { - OrderAmendment, - OrderAmendmentBody, - OrderCancellation, - OrderCancellationBody, - OrderSubmission, - OrderSubmissionBody, - Transaction, -} from '@vegaprotocol/wallet'; - -export const testOrderSubmission = ( - order: OrderSubmission, - expected?: Partial -) => { - const expectedOrder = { - ...order, - ...expected, - }; - - const transaction: OrderSubmissionBody = { - orderSubmission: expectedOrder, - }; - vegaWalletTransaction(transaction); - verifyToast(); -}; - -export const testOrderAmendment = ( - order: OrderAmendment, - expected?: Partial -) => { - const expectedOrder = { - ...order, - ...expected, - }; - - const transaction: OrderAmendmentBody = { - orderAmendment: expectedOrder, - }; - vegaWalletTransaction(transaction); - verifyToast(); -}; - -export const testOrderCancellation = ( - order: OrderCancellation, - expected?: Partial -) => { - 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(); -}; diff --git a/apps/trading-e2e/src/support/trading.ts b/apps/trading-e2e/src/support/trading.ts deleted file mode 100644 index 4cf8a2dde..000000000 --- a/apps/trading-e2e/src/support/trading.ts +++ /dev/null @@ -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 => ({ - 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 => ({ - 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 { - 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], - } - ); - } - ); -}; diff --git a/apps/trading-e2e/tsconfig.json b/apps/trading-e2e/tsconfig.json deleted file mode 100644 index b05c02c2f..000000000 --- a/apps/trading-e2e/tsconfig.json +++ /dev/null @@ -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"] -} diff --git a/apps/trading/README.md b/apps/trading/README.md index a2d26e06c..244b92f55 100644 --- a/apps/trading/README.md +++ b/apps/trading/README.md @@ -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) diff --git a/tools/ci/check-affected.py b/tools/ci/check-affected.py index e3ee84ff9..6e35f3f98 100644 --- a/tools/ci/check-affected.py +++ b/tools/ci/check-affected.py @@ -84,6 +84,9 @@ if 'release' in args.github_ref: projects = json.dumps(projects) + +# The trading project does not use the deafult NX e2e setup (cypress) +projects_e2e.remove('trading-e2e') projects_e2e = json.dumps(projects_e2e) print(f'Projects: {projects}') From e6b3ff456d956032bebd541069bffdd622a4a666 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:58:33 +0100 Subject: [PATCH 21/21] fix(trading): fix tests due to new wallet connection method (#5903) --- .../src/integration/flow/staking-flow.cy.ts | 2 ++ .../flow/token-association-flow.cy.ts | 2 ++ .../src/integration/view/home.cy.ts | 6 ++--- .../src/integration/view/validators.cy.ts | 2 ++ .../src/integration/view/wallet-vega.cy.ts | 23 ++++++++----------- .../src/support/common.functions.ts | 6 +++++ .../cypress/src/lib/commands/mock-chain-id.ts | 2 +- .../src/lib/commands/vega-wallet-connect.ts | 4 +++- libs/cypress/src/lib/mock-rest.ts | 5 ++-- 9 files changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts index 985844e09..07cfb79d6 100644 --- a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts @@ -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'); diff --git a/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts b/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts index af57804e5..212420c2c 100644 --- a/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts @@ -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(); diff --git a/apps/governance-e2e/src/integration/view/home.cy.ts b/apps/governance-e2e/src/integration/view/home.cy.ts index 0c77b8162..47ce0e4a6 100644 --- a/apps/governance-e2e/src/integration/view/home.cy.ts +++ b/apps/governance-e2e/src/integration/view/home.cy.ts @@ -82,20 +82,20 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () { it('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 () { 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'); }); diff --git a/apps/governance-e2e/src/integration/view/validators.cy.ts b/apps/governance-e2e/src/integration/view/validators.cy.ts index 791bf01d4..149866a8c 100644 --- a/apps/governance-e2e/src/integration/view/validators.cy.ts +++ b/apps/governance-e2e/src/integration/view/validators.cy.ts @@ -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); }); diff --git a/apps/governance-e2e/src/integration/view/wallet-vega.cy.ts b/apps/governance-e2e/src/integration/view/wallet-vega.cy.ts index 58011cc60..14a7c5d72 100644 --- a/apps/governance-e2e/src/integration/view/wallet-vega.cy.ts +++ b/apps/governance-e2e/src/integration/view/wallet-vega.cy.ts @@ -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(); }); diff --git a/apps/governance-e2e/src/support/common.functions.ts b/apps/governance-e2e/src/support/common.functions.ts index c440176de..4ac18831e 100644 --- a/apps/governance-e2e/src/support/common.functions.ts +++ b/apps/governance-e2e/src/support/common.functions.ts @@ -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') diff --git a/libs/cypress/src/lib/commands/mock-chain-id.ts b/libs/cypress/src/lib/commands/mock-chain-id.ts index 1c90f7938..e510a661b 100644 --- a/libs/cypress/src/lib/commands/mock-chain-id.ts +++ b/libs/cypress/src/lib/commands/mock-chain-id.ts @@ -12,7 +12,7 @@ declare global { } } -const chainId = 'test-id'; +const chainId = 'vega-stagnet1-202307191148'; export function addMockChainId() { Cypress.Commands.add('mockChainId', () => { diff --git a/libs/cypress/src/lib/commands/vega-wallet-connect.ts b/libs/cypress/src/lib/commands/vega-wallet-connect.ts index d3e6e9486..63c5049bc 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-connect.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-connect.ts @@ -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'); diff --git a/libs/cypress/src/lib/mock-rest.ts b/libs/cypress/src/lib/mock-rest.ts index 359a37a48..f83306a65 100644 --- a/libs/cypress/src/lib/mock-rest.ts +++ b/libs/cypress/src/lib/mock-rest.ts @@ -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', },