{children &&
diff --git a/src/renderer/components/common/QrCode/QrReader/QrReaderWrapper.tsx b/src/renderer/components/common/QrCode/QrReader/QrReaderWrapper.tsx
index 949f351a52..74483a61e6 100644
--- a/src/renderer/components/common/QrCode/QrReader/QrReaderWrapper.tsx
+++ b/src/renderer/components/common/QrCode/QrReader/QrReaderWrapper.tsx
@@ -1,8 +1,8 @@
import cn from 'classnames';
import React, { useEffect, useState } from 'react';
-import { cnTw, ValidationErrors, secondsToMinutes } from '@renderer/shared/lib/utils';
-import { Shimmering, Button, CaptionText, FootnoteText, Select, SmallTitleText } from '@renderer/shared/ui';
+import { cnTw, ValidationErrors } from '@renderer/shared/lib/utils';
+import { Shimmering, Button, CaptionText, FootnoteText, Select, SmallTitleText, Countdown } from '@renderer/shared/ui';
import { DropdownOption, DropdownResult } from '@renderer/shared/ui/Dropdowns/common/types';
import { useI18n } from '@renderer/app/providers';
import SignatureReaderError from './SignatureReaderError';
@@ -129,20 +129,7 @@ const QrReaderWrapper = ({ className, onResult, countdown, validationError, isMu
{t('signing.scanQrTitle')}
- {/* countdown */}
-
- {t('signing.qrCountdownTitle')}
- = 60 ? 'bg-label-background-green' : 'bg-label-background-red'),
- )}
- >
- {secondsToMinutes(countdown)}
-
-
+
{/* scanning frame */}
diff --git a/src/renderer/entities/chain/ui/ChainIcon/ChainIcon.test.tsx b/src/renderer/entities/chain/ui/ChainIcon/ChainIcon.test.tsx
index 4e23e762f7..8f43aea409 100644
--- a/src/renderer/entities/chain/ui/ChainIcon/ChainIcon.test.tsx
+++ b/src/renderer/entities/chain/ui/ChainIcon/ChainIcon.test.tsx
@@ -3,6 +3,15 @@ import { act, render, screen } from '@testing-library/react';
import { ChainIcon } from './ChainIcon';
import { TEST_CHAIN_ICON } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/ChainIcon', () => {
test('should render component', async () => {
await act(async () => {
diff --git a/src/renderer/entities/chain/ui/ChainTitle/ChainTitle.test.tsx b/src/renderer/entities/chain/ui/ChainTitle/ChainTitle.test.tsx
index e9c665b59d..c5dca6f8f6 100644
--- a/src/renderer/entities/chain/ui/ChainTitle/ChainTitle.test.tsx
+++ b/src/renderer/entities/chain/ui/ChainTitle/ChainTitle.test.tsx
@@ -3,6 +3,15 @@ import { act, render, screen } from '@testing-library/react';
import { ChainTitle } from './ChainTitle';
import { TEST_CHAIN_ID } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/ChainTitle', () => {
test('should render component', async () => {
await act(async () => {
diff --git a/src/renderer/entities/chain/ui/XcmChains/XcmChains.test.tsx b/src/renderer/entities/chain/ui/XcmChains/XcmChains.test.tsx
index ec13b799dc..6e791e73f0 100644
--- a/src/renderer/entities/chain/ui/XcmChains/XcmChains.test.tsx
+++ b/src/renderer/entities/chain/ui/XcmChains/XcmChains.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import { XcmChains } from './XcmChains';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
jest.mock('../ChainTitle/ChainTitle', () => ({
ChainTitle: ({ chainId }: any) =>
{chainId},
}));
diff --git a/src/renderer/entities/operation/ui/SignButton.tsx b/src/renderer/entities/operation/ui/SignButton.tsx
new file mode 100644
index 0000000000..868ab28ea6
--- /dev/null
+++ b/src/renderer/entities/operation/ui/SignButton.tsx
@@ -0,0 +1,48 @@
+import { useI18n } from '@renderer/app/providers';
+import { WalletType } from '@renderer/shared/core';
+import { Button, Icon } from '@renderer/shared/ui';
+import { IconNames } from '@renderer/shared/ui/Icon/data';
+
+type Props = {
+ type: WalletType;
+ disabled?: boolean;
+ onClick?: () => void;
+ className?: string;
+};
+
+const WalletIcon: Record
= {
+ [WalletType.POLKADOT_VAULT]: 'vault',
+ [WalletType.MULTISIG]: 'vault',
+ [WalletType.WATCH_ONLY]: 'watchOnly',
+ [WalletType.WALLET_CONNECT]: 'walletConnect',
+ [WalletType.NOVA_WALLET]: 'novaWallet',
+ // legacy
+ [WalletType.MULTISHARD_PARITY_SIGNER]: 'vault',
+ [WalletType.SINGLE_PARITY_SIGNER]: 'vault',
+};
+
+const WalletText: Record = {
+ [WalletType.POLKADOT_VAULT]: 'operation.polkadotVault',
+ [WalletType.MULTISIG]: 'operation.polkadotVault',
+ [WalletType.WATCH_ONLY]: 'operation.sign.watchOnly',
+ [WalletType.WALLET_CONNECT]: 'operation.sign.walletConnect',
+ [WalletType.NOVA_WALLET]: 'operation.sign.novaWallet',
+ // legacy
+ [WalletType.MULTISHARD_PARITY_SIGNER]: 'operation.sign.polkadotVault',
+ [WalletType.SINGLE_PARITY_SIGNER]: 'operation.sign.polkadotVault',
+};
+
+export const SignButton = ({ disabled, type, onClick, className }: Props) => {
+ const { t } = useI18n();
+
+ return (
+ }
+ onClick={onClick}
+ >
+ {t(WalletText[type])}
+
+ );
+};
diff --git a/src/renderer/entities/transaction/ui/OperationResult/OperationResult.test.tsx b/src/renderer/entities/transaction/ui/OperationResult/OperationResult.test.tsx
index a1aa367d80..86dafa4ea6 100644
--- a/src/renderer/entities/transaction/ui/OperationResult/OperationResult.test.tsx
+++ b/src/renderer/entities/transaction/ui/OperationResult/OperationResult.test.tsx
@@ -3,6 +3,15 @@ import noop from 'lodash/noop';
import { OperationResult } from './OperationResult';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
jest.mock('@renderer/shared/ui/Animation/Animation', () => ({
Animation: () => animation,
}));
diff --git a/src/renderer/entities/transaction/ui/OperationResult/OperationResult.tsx b/src/renderer/entities/transaction/ui/OperationResult/OperationResult.tsx
index d6925d878e..afa4e8e753 100644
--- a/src/renderer/entities/transaction/ui/OperationResult/OperationResult.tsx
+++ b/src/renderer/entities/transaction/ui/OperationResult/OperationResult.tsx
@@ -1,12 +1,9 @@
-import { Fragment, PropsWithChildren, useEffect, useState } from 'react';
-import { Dialog, Transition } from '@headlessui/react';
+import { PropsWithChildren } from 'react';
-import { ModalBackdrop, ModalTransition } from '@renderer/shared/ui/Modals/common';
-import { FootnoteText, SmallTitleText } from '@renderer/shared/ui';
+import { StatusModal } from '@renderer/shared/ui';
import { Animation } from '@renderer/shared/ui/Animation/Animation';
import { VariantAnimationProps } from './common/constants';
import { Variant } from './common/types';
-import Animations from '@renderer/shared/ui/Animation/Data';
type Props = {
title: string;
@@ -24,38 +21,15 @@ export const OperationResult = ({
children,
onClose,
}: PropsWithChildren) => {
- const [animation, setAnimation] = useState();
-
- useEffect(() => {
- if (isOpen) {
- // using same animation repeatedly without deep clone lead to memory leak
- // https://github.com/airbnb/lottie-web/issues/1159
- setAnimation(JSON.parse(JSON.stringify(Animations[variant])));
- }
- }, [isOpen, variant]);
-
return (
-
-
-
+ }
+ title={title}
+ description={description}
+ isOpen={isOpen}
+ onClose={onClose}
+ >
+ {children}
+
);
};
diff --git a/src/renderer/entities/wallet/lib/__tests__/model-utils.test.ts b/src/renderer/entities/wallet/lib/__tests__/model-utils.test.ts
index fb09cf4a13..18d3a8ffb9 100644
--- a/src/renderer/entities/wallet/lib/__tests__/model-utils.test.ts
+++ b/src/renderer/entities/wallet/lib/__tests__/model-utils.test.ts
@@ -2,6 +2,15 @@ import { modelUtils } from '../model-utils';
import { AccountType, ChainType, CryptoType, KeyType, BaseAccount, ChainAccount } from '@renderer/shared/core';
import { TEST_ACCOUNT_ID, TEST_CHAIN_ID } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
const accounts = [
{
name: 'My base account',
diff --git a/src/renderer/entities/wallet/lib/account-utils.ts b/src/renderer/entities/wallet/lib/account-utils.ts
index 463d2ac8ec..cad00534cb 100644
--- a/src/renderer/entities/wallet/lib/account-utils.ts
+++ b/src/renderer/entities/wallet/lib/account-utils.ts
@@ -2,13 +2,22 @@ import { u8aToHex } from '@polkadot/util';
import { createKeyMulti } from '@polkadot/util-crypto';
import { AccountType, ChainId } from '@renderer/shared/core';
-import type { AccountId, Threshold, MultisigAccount, Account, BaseAccount, ChainAccount } from '@renderer/shared/core';
+import type {
+ AccountId,
+ Threshold,
+ MultisigAccount,
+ Account,
+ BaseAccount,
+ ChainAccount,
+ WalletConnectAccount,
+} from '@renderer/shared/core';
export const accountUtils = {
isBaseAccount,
isChainAccount,
isMultisigAccount,
isChainIdMatch,
+ isWalletConnectAccount,
getMultisigAccountId,
};
@@ -24,10 +33,18 @@ function isChainAccount(account: Pick): account is ChainAccount
return account.type === AccountType.CHAIN;
}
-function isChainIdMatch(account: Pick, chainId: ChainId): boolean {
- return !isChainAccount(account) || account.chainId === chainId;
+function isWalletConnectAccount(account: Pick): account is WalletConnectAccount {
+ return account.type === AccountType.WALLET_CONNECT;
}
+function isChainIdMatch(account: Pick, chainId: ChainId): boolean {
+ if (isBaseAccount(account) || isMultisigAccount(account)) return true;
+
+ const chainAccountMatch = isChainAccount(account) && account.chainId === chainId;
+ const walletConnectAccountMatch = isWalletConnectAccount(account) && account.chainId === chainId;
+
+ return chainAccountMatch || walletConnectAccountMatch;
+}
function isMultisigAccount(account: Pick): account is MultisigAccount {
return account.type === AccountType.MULTISIG;
}
diff --git a/src/renderer/entities/wallet/lib/wallet-utils.ts b/src/renderer/entities/wallet/lib/wallet-utils.ts
index 5fb1145f72..31639ef8a3 100644
--- a/src/renderer/entities/wallet/lib/wallet-utils.ts
+++ b/src/renderer/entities/wallet/lib/wallet-utils.ts
@@ -7,6 +7,8 @@ export const walletUtils = {
isSingleShard,
isMultisig,
isWatchOnly,
+ isNovaWallet,
+ isWalletConnect,
};
function isPolkadotVault(wallet?: Wallet | null): boolean {
@@ -35,3 +37,11 @@ function isMultisig(wallet?: Wallet | null): boolean {
function isWatchOnly(wallet?: Wallet | null): boolean {
return wallet?.type === WalletType.WATCH_ONLY;
}
+
+function isNovaWallet(wallet?: Wallet | null): boolean {
+ return wallet?.type === WalletType.NOVA_WALLET;
+}
+
+function isWalletConnect(wallet?: Wallet | null): boolean {
+ return wallet?.type === WalletType.WALLET_CONNECT;
+}
diff --git a/src/renderer/entities/wallet/model/__tests__/wallet-model.test.ts b/src/renderer/entities/wallet/model/__tests__/wallet-model.test.ts
index d31e8da218..216d11f213 100644
--- a/src/renderer/entities/wallet/model/__tests__/wallet-model.test.ts
+++ b/src/renderer/entities/wallet/model/__tests__/wallet-model.test.ts
@@ -5,6 +5,15 @@ import { walletMock } from './mocks/wallet-mock';
import { kernelModel } from '@renderer/shared/core';
import { storageService } from '@renderer/shared/api/storage';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('entities/wallet/model/wallet-model', () => {
afterEach(() => {
jest.clearAllMocks();
diff --git a/src/renderer/entities/wallet/model/wallet-model.ts b/src/renderer/entities/wallet/model/wallet-model.ts
index d852213dff..a0e8058f0c 100644
--- a/src/renderer/entities/wallet/model/wallet-model.ts
+++ b/src/renderer/entities/wallet/model/wallet-model.ts
@@ -2,7 +2,7 @@ import { createStore, createEvent, forward, createEffect, sample, combine } from
import { spread } from 'patronum';
import type { Wallet, NoID, Account, BaseAccount, ChainAccount, MultisigAccount } from '@renderer/shared/core';
-import { kernelModel } from '@renderer/shared/core';
+import { kernelModel, WalletConnectAccount } from '@renderer/shared/core';
import { storageService } from '@renderer/shared/api/storage';
import { modelUtils } from '../lib/model-utils';
@@ -29,6 +29,8 @@ const watchOnlyCreated = createEvent>();
const multishardCreated = createEvent>();
const singleshardCreated = createEvent>();
const multisigCreated = createEvent>();
+const walletConnectCreated = createEvent>();
+
const walletSelected = createEvent();
const multisigAccountUpdated = createEvent();
@@ -46,7 +48,7 @@ type CreateResult = {
};
const walletCreatedFx = createEffect(
- async ({ wallet, accounts }: CreateParams): Promise => {
+ async ({ wallet, accounts }: CreateParams): Promise => {
const dbWallet = await storageService.wallets.create({ ...wallet, isActive: false });
if (!dbWallet) return undefined;
@@ -150,6 +152,11 @@ sample({
target: $wallets,
});
+forward({
+ from: walletConnectCreated,
+ to: walletCreatedFx,
+});
+
forward({
from: [watchOnlyCreated, multisigCreated, singleshardCreated],
to: walletCreatedFx,
@@ -201,6 +208,7 @@ export const walletModel = {
multishardCreated,
singleshardCreated,
multisigCreated,
+ walletConnectCreated,
walletSelected,
multisigAccountUpdated,
},
diff --git a/src/renderer/entities/wallet/ui/AccountAddress/AccountAddress.test.tsx b/src/renderer/entities/wallet/ui/AccountAddress/AccountAddress.test.tsx
index 78a24a0c8f..13b1a84ef4 100644
--- a/src/renderer/entities/wallet/ui/AccountAddress/AccountAddress.test.tsx
+++ b/src/renderer/entities/wallet/ui/AccountAddress/AccountAddress.test.tsx
@@ -3,6 +3,15 @@ import { render, screen } from '@testing-library/react';
import { AccountAddress } from './AccountAddress';
import { TEST_ACCOUNT_ID, TEST_ADDRESS } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/AccountAddress', () => {
test('should render component', () => {
render();
diff --git a/src/renderer/entities/wallet/ui/AddressWithName/AddressWithName.test.tsx b/src/renderer/entities/wallet/ui/AddressWithName/AddressWithName.test.tsx
index 27fb0d9c18..85ea5aa343 100644
--- a/src/renderer/entities/wallet/ui/AddressWithName/AddressWithName.test.tsx
+++ b/src/renderer/entities/wallet/ui/AddressWithName/AddressWithName.test.tsx
@@ -3,6 +3,15 @@ import { render, screen } from '@testing-library/react';
import { AddressWithName } from './AddressWithName';
import { TEST_ACCOUNT_ID, TEST_ADDRESS } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Address', () => {
test('should render component', () => {
render();
diff --git a/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx b/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx
new file mode 100644
index 0000000000..94d2e00b34
--- /dev/null
+++ b/src/renderer/entities/wallet/ui/MultiAccountsList/MultiAccountsList.tsx
@@ -0,0 +1,52 @@
+import { cnTw } from '@renderer/shared/lib/utils';
+import { ChainTitle } from '@renderer/entities/chain';
+import { useI18n } from '@renderer/app/providers';
+import { FootnoteText } from '@renderer/shared/ui';
+import { AccountId, Chain } from '@renderer/shared/core';
+import { AddressWithExplorers } from '@renderer/entities/wallet';
+
+type Props = {
+ accounts: {
+ chain: Chain;
+ accountId: AccountId;
+ }[];
+ className?: string;
+};
+
+export const MultiAccountsList = ({ accounts, className }: Props) => {
+ const { t } = useI18n();
+
+ return (
+ <>
+
+
+ {t('accountList.networksColumn', { chains: accounts.length })}
+
+ {t('accountList.addressColumn')}
+
+
+
+ {accounts.map(({ chain, accountId }) => {
+ const { chainId, addressPrefix, explorers } = chain;
+
+ return (
+ -
+
+
+
+
+ );
+ })}
+
+ >
+ );
+};
diff --git a/src/renderer/entities/wallet/ui/index.ts b/src/renderer/entities/wallet/ui/index.ts
index 687f311154..4a701e4307 100644
--- a/src/renderer/entities/wallet/ui/index.ts
+++ b/src/renderer/entities/wallet/ui/index.ts
@@ -3,4 +3,5 @@ export { AccountsList } from './AccountsList/AccountsList';
export { AddressWithName } from './AddressWithName/AddressWithName';
export { AccountAddress, getAddress } from './AccountAddress/AccountAddress';
export { AddressWithTwoLines } from './AddressWithTwoLines/AddressWithTwoLines';
+export { MultiAccountsList } from './MultiAccountsList/MultiAccountsList';
export type { AccountAddressProps } from './AccountAddress/AccountAddress';
diff --git a/src/renderer/entities/walletConnect/index.ts b/src/renderer/entities/walletConnect/index.ts
new file mode 100644
index 0000000000..a715b06e3b
--- /dev/null
+++ b/src/renderer/entities/walletConnect/index.ts
@@ -0,0 +1,2 @@
+export * from './lib';
+export * from './model/wallet-connect-model';
diff --git a/src/renderer/entities/walletConnect/lib/common/constants.ts b/src/renderer/entities/walletConnect/lib/common/constants.ts
new file mode 100644
index 0000000000..f2d5272ea1
--- /dev/null
+++ b/src/renderer/entities/walletConnect/lib/common/constants.ts
@@ -0,0 +1,51 @@
+export const DEFAULT_PROJECT_ID = 'af50115ecc7e992a0ef4a577daf5c1c8';
+export const DEFAULT_RELAY_URL = 'wss://relay.walletconnect.com';
+
+export const DEFAULT_LOGGER = 'debug';
+
+export const DEFAULT_APP_METADATA = {
+ name: 'Nova Spektr', //dApp name
+ description: 'Full-spectrum Polkadot Desktop Wallet', //dApp description
+ url: 'https://novaspektr.io', //dApp url
+ icons: ['https://drive.google.com/uc?id=1oud8FHw3PcldUgHVeX5OjCg8XANhGO5s'], //dApp logo url
+ verifyUrl: 'https://verify.walletconnect.com',
+};
+
+/**
+ * POLKADOT
+ */
+export enum DEFAULT_POLKADOT_METHODS {
+ POLKADOT_SIGN_TRANSACTION = 'polkadot_signTransaction',
+}
+
+export enum DEFAULT_POLKADOT_EVENTS {
+ CHAIN_CHANGED = 'chainChanged',
+ ACCOUNTS_CHANGED = 'accountsChanged',
+}
+
+type RelayerType = {
+ value: string | undefined;
+ label: string;
+};
+
+export const REGIONALIZED_RELAYER_ENDPOINTS: RelayerType[] = [
+ {
+ value: DEFAULT_RELAY_URL,
+ label: 'Default',
+ },
+
+ {
+ value: 'wss://us-east-1.relay.walletconnect.com',
+ label: 'US',
+ },
+ {
+ value: 'wss://eu-central-1.relay.walletconnect.com',
+ label: 'EU',
+ },
+ {
+ value: 'wss://ap-southeast-1.relay.walletconnect.com',
+ label: 'Asia Pacific',
+ },
+];
+
+export const WALLETCONNECT_CLIENT_ID = 'WALLETCONNECT_CLIENT_ID';
diff --git a/src/renderer/entities/walletConnect/lib/common/types.ts b/src/renderer/entities/walletConnect/lib/common/types.ts
new file mode 100644
index 0000000000..02bba98a5a
--- /dev/null
+++ b/src/renderer/entities/walletConnect/lib/common/types.ts
@@ -0,0 +1,19 @@
+import Client from '@walletconnect/sign-client';
+import { SessionTypes } from '@walletconnect/types';
+
+export type InitConnectProps = {
+ client: Client;
+ chains: string[];
+ pairing?: any;
+};
+
+export type ConnectProps = {
+ client: Client;
+ approval: () => Promise;
+ onConnect?: () => void;
+};
+
+export type DisconnectProps = {
+ client: Client;
+ session: SessionTypes.Struct;
+};
diff --git a/src/renderer/entities/walletConnect/lib/common/utils.ts b/src/renderer/entities/walletConnect/lib/common/utils.ts
new file mode 100644
index 0000000000..5e9e1e20db
--- /dev/null
+++ b/src/renderer/entities/walletConnect/lib/common/utils.ts
@@ -0,0 +1,5 @@
+import { Chain } from '@renderer/shared/core';
+
+export const getWalletConnectChains = (chains: Chain[]) => {
+ return chains.map((c) => `polkadot:${c.chainId.slice(2, 34)}`);
+};
diff --git a/src/renderer/entities/walletConnect/lib/index.ts b/src/renderer/entities/walletConnect/lib/index.ts
new file mode 100644
index 0000000000..5bbc83f7e1
--- /dev/null
+++ b/src/renderer/entities/walletConnect/lib/index.ts
@@ -0,0 +1,3 @@
+export * from './common/utils';
+export * from './common/types';
+export * from './common/constants';
diff --git a/src/renderer/entities/walletConnect/model/wallet-connect-model.ts b/src/renderer/entities/walletConnect/model/wallet-connect-model.ts
new file mode 100644
index 0000000000..700c4085f6
--- /dev/null
+++ b/src/renderer/entities/walletConnect/model/wallet-connect-model.ts
@@ -0,0 +1,323 @@
+import Client from '@walletconnect/sign-client';
+import { PairingTypes, SessionTypes } from '@walletconnect/types';
+import { createEffect, createEvent, createStore, forward, sample, scopeBind } from 'effector';
+import { getSdkError } from '@walletconnect/utils';
+import keyBy from 'lodash/keyBy';
+
+import { nonNullable } from '@renderer/shared/lib/utils';
+import {
+ ConnectProps,
+ DEFAULT_APP_METADATA,
+ DEFAULT_LOGGER,
+ DEFAULT_POLKADOT_EVENTS,
+ DEFAULT_POLKADOT_METHODS,
+ DEFAULT_PROJECT_ID,
+ DEFAULT_RELAY_URL,
+ DisconnectProps,
+ InitConnectProps,
+ WALLETCONNECT_CLIENT_ID,
+} from '../lib';
+import { Account, kernelModel } from '@renderer/shared/core';
+import { localStorageService } from '@renderer/shared/api/local-storage';
+import { walletModel } from '@renderer/entities/wallet/model/wallet-model';
+import { storageService } from '@renderer/shared/api/storage';
+
+type InitConnectResult = {
+ uri: string | undefined;
+ approval: () => Promise;
+};
+
+type ConnectResult = {
+ pairings: PairingTypes.Struct[];
+ session: SessionTypes.Struct;
+};
+
+type SessionTopicUpdateProps = {
+ activeAccounts: Account[];
+ sessionTopic: string;
+};
+
+const connect = createEvent>();
+const disconnect = createEvent();
+const reset = createEvent();
+const sessionUpdated = createEvent();
+const connected = createEvent();
+const rejectConnection = createEvent();
+const sessionTopicUpdated = createEvent();
+
+const $client = createStore(null).reset(reset);
+const $session = createStore(null).reset(reset);
+const $uri = createStore('').reset(reset);
+const $accounts = createStore([]).reset(reset);
+const $pairings = createStore([]).reset(reset);
+
+const subscribeToEventsFx = createEffect((client: Client) => {
+ const bindedSessionUpdated = scopeBind(sessionUpdated);
+ const bindedReset = scopeBind(reset);
+
+ client.on('session_update', ({ topic, params }) => {
+ console.log('WC EVENT', 'session_update', { topic, params });
+ const { namespaces } = params;
+ const _session = client.session.get(topic);
+ const updatedSession = { ..._session, namespaces };
+
+ bindedSessionUpdated(updatedSession);
+ });
+
+ client.on('session_ping', (args) => {
+ console.log('WC EVENT', 'session_ping', args);
+ });
+
+ client.on('session_event', (args) => {
+ console.log('WC EVENT', 'session_event', args);
+ });
+
+ client.on('session_delete', () => {
+ console.log('WC EVENT', 'session_delete');
+ bindedReset();
+ });
+});
+
+const checkPersistedStateFx = createEffect(async (client: Client) => {
+ if (!client) return;
+
+ const pairings = client.pairing.getAll({ active: true });
+
+ // Set pairings
+ console.log('RESTORED PAIRINGS: ', pairings);
+
+ if (client.session.length) {
+ const lastKeyIndex = client.session.keys.length - 1;
+ const session = client.session.get(client.session.keys[lastKeyIndex]);
+ console.log('RESTORED SESSION:', session);
+
+ sessionUpdated(session);
+ }
+});
+
+const logClientIdFx = createEffect(async (client: Client) => {
+ if (!client) return;
+
+ try {
+ const clientId = await client.core.crypto.getClientId();
+ console.log('WalletConnect ClientID: ', clientId);
+ localStorageService.saveToStorage(WALLETCONNECT_CLIENT_ID, clientId);
+ } catch (error) {
+ console.error('Failed to set WalletConnect clientId in localStorage: ', error);
+ }
+});
+
+const sessionTopicUpdatedFx = createEffect(
+ async ({ activeAccounts, sessionTopic }: SessionTopicUpdateProps): Promise => {
+ const updatedAccounts = activeAccounts.map(({ signingExtras, ...rest }) => {
+ const newSigningExtras = { ...signingExtras, sessionTopic };
+
+ return { ...rest, signingExtras: newSigningExtras } as Account;
+ });
+ const updated = await storageService.accounts.updateAll(updatedAccounts);
+
+ return updated && updatedAccounts;
+ },
+);
+
+const createClientFx = createEffect(async (): Promise => {
+ try {
+ return Client.init({
+ logger: DEFAULT_LOGGER,
+ relayUrl: DEFAULT_RELAY_URL,
+ projectId: DEFAULT_PROJECT_ID,
+ metadata: DEFAULT_APP_METADATA,
+ });
+ } catch (e) {
+ console.log(`Failed to create new Client`, e);
+ }
+});
+
+forward({
+ from: kernelModel.events.appStarted,
+ to: createClientFx,
+});
+
+sample({
+ clock: createClientFx.doneData,
+ filter: (client): client is Client => client !== null,
+ target: [subscribeToEventsFx, checkPersistedStateFx, logClientIdFx],
+});
+
+const initConnectFx = createEffect(
+ async ({ client, chains, pairing }: InitConnectProps): Promise => {
+ try {
+ const optionalNamespaces = {
+ polkadot: {
+ methods: [DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION],
+ chains,
+ events: [DEFAULT_POLKADOT_EVENTS.CHAIN_CHANGED, DEFAULT_POLKADOT_EVENTS.ACCOUNTS_CHANGED],
+ },
+ };
+
+ const { uri, approval } = await client.connect({
+ pairingTopic: pairing?.topic,
+ optionalNamespaces,
+ });
+
+ return {
+ uri,
+ approval,
+ };
+ } catch (e) {
+ console.log(`Failed to init connection`, e);
+ }
+ },
+);
+
+const connectFx = createEffect(async ({ client, approval }: ConnectProps): Promise => {
+ try {
+ const session = await approval();
+ console.log('Established session:', session);
+
+ return {
+ pairings: client.pairing.getAll({ active: true }),
+ session: session as SessionTypes.Struct,
+ };
+ } catch (e: any) {
+ rejectConnection(e);
+ }
+});
+
+const disconnectFx = createEffect(async ({ client, session }: DisconnectProps) => {
+ try {
+ await client.disconnect({
+ topic: session.topic,
+ reason: getSdkError('USER_DISCONNECTED'),
+ });
+ } catch (error) {
+ return;
+ }
+});
+
+forward({
+ from: sessionUpdated,
+ to: $session,
+});
+
+sample({
+ clock: createClientFx.doneData,
+ filter: (client) => nonNullable(client),
+ fn: (client) => client!,
+ target: $client,
+});
+
+sample({
+ clock: connect,
+ source: $client,
+ filter: (client, props) => client !== null && props.chains.length > 0,
+ fn: (client, props) => ({
+ client: client!,
+ ...props,
+ }),
+ target: initConnectFx,
+});
+
+sample({
+ clock: initConnectFx.doneData,
+ source: $client,
+ filter: (client, initData) => client !== null && initData?.approval !== null,
+ fn: ($client, initData) => ({
+ client: $client!,
+ approval: initData?.approval!,
+ }),
+ target: connectFx,
+});
+
+sample({
+ clock: initConnectFx.doneData,
+ fn: (data) => data?.uri || '',
+ target: $uri,
+});
+
+sample({
+ clock: connectFx.doneData,
+ filter: (props) => nonNullable(props),
+ fn: (props) => {
+ return Object.values(props!.session.namespaces)
+ .map((namespace) => namespace.accounts)
+ .flat();
+ },
+ target: $accounts,
+});
+
+sample({
+ clock: connectFx.doneData,
+ filter: (props) => nonNullable(props),
+ fn: (props) => props!.session,
+ target: $session,
+});
+
+sample({
+ clock: connectFx.doneData,
+ filter: (props) => nonNullable(props),
+ fn: (props) => props!.pairings,
+ target: $pairings,
+});
+
+forward({
+ from: connectFx.done,
+ to: connected,
+});
+
+sample({
+ clock: disconnect,
+ source: {
+ client: $client,
+ session: $session,
+ },
+ filter: ({ client, session }) => client !== null && session !== null,
+ fn: ({ client, session }) => ({
+ client: client!,
+ session: session!,
+ }),
+ target: disconnectFx,
+});
+
+forward({
+ from: disconnectFx.done,
+ to: reset,
+});
+
+sample({
+ clock: sessionTopicUpdated,
+ source: walletModel.$activeAccounts,
+ fn: (activeAccounts, sessionTopic) => ({
+ activeAccounts,
+ sessionTopic,
+ }),
+ target: sessionTopicUpdatedFx,
+});
+
+sample({
+ clock: sessionTopicUpdatedFx.doneData,
+ source: walletModel.$accounts,
+ fn: (accounts, updatedAccounts) => {
+ const updatedMap = keyBy(updatedAccounts, 'id');
+
+ return accounts.map((account) => updatedMap[account.id] || account);
+ },
+ target: walletModel.$accounts,
+});
+
+export const walletConnectModel = {
+ $client,
+ $session,
+ $uri,
+ $accounts,
+ $pairings,
+ events: {
+ connect,
+ disconnect,
+ sessionUpdated,
+ connected,
+ rejectConnection,
+ sessionTopicUpdated,
+ reset,
+ },
+};
diff --git a/src/renderer/features/operation/sign/ui/Signing/Signing.tsx b/src/renderer/features/operation/sign/ui/Signing/Signing.tsx
index 0be71aabd3..6369932a42 100644
--- a/src/renderer/features/operation/sign/ui/Signing/Signing.tsx
+++ b/src/renderer/features/operation/sign/ui/Signing/Signing.tsx
@@ -2,6 +2,7 @@ import { useUnit } from 'effector-react';
import { SigningProps } from '../../model';
import { VaultSigning } from '../VaultSigning/VaultSigning';
+import { WalletConnect } from '../WalletConnect/WalletConnect';
import { SigningType } from '@renderer/shared/core';
import { walletModel } from '@renderer/entities/wallet';
@@ -9,6 +10,7 @@ export const SigningFlow: Record JSX.Eleme
[SigningType.MULTISIG]: (props) => ,
[SigningType.PARITY_SIGNER]: (props) => ,
[SigningType.WATCH_ONLY]: () => null,
+ [SigningType.WALLET_CONNECT]: (props) => ,
};
export const Signing = (props: SigningProps) => {
diff --git a/src/renderer/features/operation/sign/ui/WalletConnect/WalletConnect.tsx b/src/renderer/features/operation/sign/ui/WalletConnect/WalletConnect.tsx
new file mode 100644
index 0000000000..837a05a83d
--- /dev/null
+++ b/src/renderer/features/operation/sign/ui/WalletConnect/WalletConnect.tsx
@@ -0,0 +1,266 @@
+import { useEffect, useState } from 'react';
+import { UnsignedTransaction } from '@substrate/txwrapper-polkadot';
+import { useUnit } from 'effector-react';
+
+import { SigningProps } from '@renderer/features/operation';
+import { ValidationErrors } from '@renderer/shared/lib/utils';
+import { useTransaction } from '@renderer/entities/transaction';
+import { useI18n } from '@renderer/app/providers';
+import { Button, ConfirmModal, Countdown, FootnoteText, SmallTitleText, StatusModal } from '@renderer/shared/ui';
+import { walletConnectModel, DEFAULT_POLKADOT_METHODS, getWalletConnectChains } from '@renderer/entities/walletConnect';
+import { chainsService } from '@renderer/entities/network';
+import { useCountdown, useToggle } from '@renderer/shared/lib/hooks';
+import wallet_connect_confirm from '@video/wallet_connect_confirm.mp4';
+import wallet_connect_confirm_webm from '@video/wallet_connect_confirm.webm';
+import { HexString } from '@renderer/shared/core';
+import { Animation } from '@renderer/shared/ui/Animation/Animation';
+
+export const WalletConnect = ({ api, validateBalance, onGoBack, accounts, transactions, onResult }: SigningProps) => {
+ const { t } = useI18n();
+ const { verifySignature, createPayload } = useTransaction();
+ const [countdown, resetCountdown] = useCountdown(api);
+
+ const session = useUnit(walletConnectModel.$session);
+ const client = useUnit(walletConnectModel.$client);
+ const connect = useUnit(walletConnectModel.events.connect);
+ const sessionUpdated = useUnit(walletConnectModel.events.sessionUpdated);
+
+ const chains = chainsService.getChainsData();
+
+ const [txPayload, setTxPayload] = useState();
+ const [unsignedTx, setUnsignedTx] = useState();
+ const [isNeedUpdate, setIsNeedUpdate] = useState(false);
+ const [isReconnectModalOpen, setIsReconnectModalOpen] = useState(false);
+ const [isReconnectingModalOpen, setIsReconnectingModalOpen] = useState(false);
+ const [isConnectedModalOpen, setIsConnectedModalOpen] = useState(false);
+ const [isRejectedStatusOpen, toggleRejectedStatus] = useToggle();
+ const [validationError, setValidationError] = useState();
+
+ const transaction = transactions[0];
+ const account = accounts[0];
+
+ useEffect(() => {
+ if (txPayload || !client) return;
+
+ const isCurrentSession = session && account && session.topic === account.signingExtras?.sessionTopic;
+
+ if (isCurrentSession) {
+ setupTransaction().catch(() => console.warn('WalletConnect | setupTransaction() failed'));
+ } else {
+ const sessions = client.session.getAll();
+
+ const storedSession = sessions.find((s) => s.topic === account.signingExtras?.sessionTopic);
+
+ if (storedSession) {
+ sessionUpdated(storedSession);
+ setIsNeedUpdate(true);
+
+ setupTransaction().catch(() => console.warn('WalletConnect | setupTransaction() failed'));
+ } else {
+ setIsReconnectModalOpen(true);
+ }
+ }
+ }, [transaction, api]);
+
+ useEffect(() => {
+ if (isNeedUpdate) {
+ setIsNeedUpdate(false);
+
+ if (session?.topic) {
+ walletConnectModel.events.sessionTopicUpdated(session?.topic);
+ }
+ }
+ }, [session]);
+
+ useEffect(() => {
+ if (unsignedTx) {
+ signTransaction();
+ }
+ }, [unsignedTx]);
+
+ useEffect(() => {
+ if (isReconnectingModalOpen && session?.topic === account.signingExtras?.sessionTopic) {
+ setIsReconnectingModalOpen(false);
+ setIsConnectedModalOpen(true);
+ }
+ }, [isReconnectingModalOpen]);
+
+ useEffect(() => {
+ if (countdown <= 0) {
+ setValidationError(ValidationErrors.EXPIRED);
+ }
+ }, [countdown]);
+
+ const setupTransaction = async (): Promise => {
+ try {
+ const { payload, unsigned } = await createPayload(transaction, api);
+
+ setTxPayload(payload);
+ setUnsignedTx(unsigned);
+
+ if (payload) {
+ resetCountdown();
+ }
+ } catch (error) {
+ console.warn(error);
+ }
+ };
+
+ const reconnect = async () => {
+ setIsReconnectModalOpen(false);
+ setIsReconnectingModalOpen(true);
+
+ connect({
+ chains: getWalletConnectChains(chains),
+ pairing: { topic: account.signingExtras?.pairingTopic },
+ });
+
+ setIsNeedUpdate(true);
+ };
+
+ const handleReconnect = () => {
+ reconnect()
+ .then(setupTransaction)
+ .catch(() => {
+ console.warn('WalletConnect | setupTransaction() failed');
+ toggleRejectedStatus();
+ });
+ };
+
+ const signTransaction = async () => {
+ if (!api || !client || !session) return;
+
+ try {
+ const result = await client.request<{
+ payload: string;
+ signature: HexString;
+ }>({
+ // eslint-disable-next-line i18next/no-literal-string
+ chainId: `polkadot:${transaction.chainId.slice(2, 34)}`,
+ topic: session.topic,
+ request: {
+ method: DEFAULT_POLKADOT_METHODS.POLKADOT_SIGN_TRANSACTION,
+ params: {
+ address: transaction.address,
+ transactionPayload: unsignedTx,
+ },
+ },
+ });
+
+ if (result.signature) {
+ handleSignature(result.signature);
+ }
+ } catch (e) {
+ console.warn(e);
+ toggleRejectedStatus();
+ }
+ };
+
+ const handleSignature = async (signature: HexString) => {
+ const isVerified = txPayload && verifySignature(txPayload, signature as HexString, accounts[0].accountId);
+
+ const balanceValidationError = validateBalance && (await validateBalance());
+
+ if (isVerified && balanceValidationError) {
+ setValidationError(balanceValidationError || ValidationErrors.INVALID_SIGNATURE);
+ } else {
+ if (unsignedTx) {
+ onResult([signature], [unsignedTx]);
+ }
+ }
+ };
+
+ const walletName = session?.peer.metadata.name || t('operation.walletConnect.defaultWalletName');
+
+ const getStatusProps = () => {
+ if (isReconnectingModalOpen) {
+ return {
+ title: t('operation.walletConnect.reconnect.reconnecting'),
+ content: ,
+ onClose: onGoBack,
+ };
+ }
+
+ if (isConnectedModalOpen) {
+ return {
+ title: t('operation.walletConnect.reconnect.connected'),
+ content: ,
+ onClose: () => setIsConnectedModalOpen(false),
+ };
+ }
+
+ if (isRejectedStatusOpen) {
+ return {
+ title: t('operation.walletConnect.rejected'),
+ content: ,
+ onClose: onGoBack,
+ };
+ }
+
+ return {
+ title: '',
+ content: <>>,
+ onClose: () => {},
+ };
+ };
+
+ return (
+
+
+ {t('operation.walletConnect.signTitle', {
+ walletName,
+ })}
+
+
+
+
+
+
+
+ {validationError === ValidationErrors.EXPIRED && (
+ <>
+
+
+ {t('operation.walletConnect.expiredDescription')}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ {t('operation.walletConnect.reconnect.title', {
+ walletName,
+ })}
+
+
+ {t('operation.walletConnect.reconnect.description')}
+
+
+
+
+
+ );
+};
diff --git a/src/renderer/features/wallets/WalletSelect/WalletMenu.tsx b/src/renderer/features/wallets/WalletSelect/WalletMenu.tsx
index dfbe150934..41fd5982f3 100644
--- a/src/renderer/features/wallets/WalletSelect/WalletMenu.tsx
+++ b/src/renderer/features/wallets/WalletSelect/WalletMenu.tsx
@@ -27,6 +27,8 @@ export const WalletMenu = ({ children }: PropsWithChildren) => {
if (walletUtils.isPolkadotVault(wallet)) groupIndex = WalletType.POLKADOT_VAULT;
if (walletUtils.isMultisig(wallet)) groupIndex = WalletType.MULTISIG;
if (walletUtils.isWatchOnly(wallet)) groupIndex = WalletType.WATCH_ONLY;
+ if (walletUtils.isNovaWallet(wallet)) groupIndex = WalletType.NOVA_WALLET;
+ if (walletUtils.isWalletConnect(wallet)) groupIndex = WalletType.WALLET_CONNECT;
if (groupIndex && includes(wallet.name, query)) {
acc[groupIndex].push(wallet);
}
@@ -37,6 +39,8 @@ export const WalletMenu = ({ children }: PropsWithChildren) => {
[WalletType.POLKADOT_VAULT]: [],
[WalletType.MULTISIG]: [],
[WalletType.WATCH_ONLY]: [],
+ [WalletType.NOVA_WALLET]: [],
+ [WalletType.WALLET_CONNECT]: [],
},
);
};
@@ -60,6 +64,18 @@ export const WalletMenu = ({ children }: PropsWithChildren) => {
onClick: () => walletProviderModel.events.walletTypeSet(WalletType.MULTISIG),
iconName: 'multisig',
},
+ {
+ id: 'nova-wallet',
+ title: t('wallets.addNovaWallet'),
+ onClick: () => walletProviderModel.events.walletTypeSet(WalletType.NOVA_WALLET),
+ iconName: 'novaWallet',
+ },
+ {
+ id: 'wallet-connect',
+ title: t('wallets.addWalletConnect'),
+ onClick: () => walletProviderModel.events.walletTypeSet(WalletType.WALLET_CONNECT),
+ iconName: 'walletConnect',
+ },
];
const selectWallet = (walletId: Wallet['id'], closeMenu: () => void) => {
diff --git a/src/renderer/features/wallets/WalletSelect/common/constants.ts b/src/renderer/features/wallets/WalletSelect/common/constants.ts
index 1c89197913..a6594a155e 100644
--- a/src/renderer/features/wallets/WalletSelect/common/constants.ts
+++ b/src/renderer/features/wallets/WalletSelect/common/constants.ts
@@ -5,6 +5,8 @@ export const GroupLabels: Record = {
[WalletType.POLKADOT_VAULT]: 'wallets.paritySignerLabel',
[WalletType.MULTISHARD_PARITY_SIGNER]: 'wallets.paritySignerLabel',
[WalletType.SINGLE_PARITY_SIGNER]: 'wallets.paritySignerLabel',
+ [WalletType.WALLET_CONNECT]: 'wallets.walletConnectLabel',
+ [WalletType.NOVA_WALLET]: 'wallets.novaWalletLabel',
[WalletType.MULTISIG]: 'wallets.multisigLabel',
[WalletType.WATCH_ONLY]: 'wallets.watchOnlyLabel',
};
@@ -13,6 +15,8 @@ export const GroupIcons: Record = {
[WalletType.POLKADOT_VAULT]: 'vault',
[WalletType.MULTISHARD_PARITY_SIGNER]: 'vault',
[WalletType.SINGLE_PARITY_SIGNER]: 'vault',
+ [WalletType.WALLET_CONNECT]: 'walletConnect',
+ [WalletType.NOVA_WALLET]: 'novaWallet',
[WalletType.MULTISIG]: 'multisig',
[WalletType.WATCH_ONLY]: 'watchOnly',
};
diff --git a/src/renderer/pages/Onboarding/WalletConnect/ManageStep/ManageStep.tsx b/src/renderer/pages/Onboarding/WalletConnect/ManageStep/ManageStep.tsx
new file mode 100644
index 0000000000..3a4d1a2e10
--- /dev/null
+++ b/src/renderer/pages/Onboarding/WalletConnect/ManageStep/ManageStep.tsx
@@ -0,0 +1,197 @@
+import cn from 'classnames';
+import { useEffect, useState } from 'react';
+import { Controller, useForm, SubmitHandler } from 'react-hook-form';
+
+import { useI18n, useStatusContext } from '@renderer/app/providers';
+import {
+ AccountId,
+ Chain,
+ ChainType,
+ ErrorType,
+ NoID,
+ SigningType,
+ WalletType,
+ AccountType,
+ WalletConnectAccount,
+} from '@renderer/shared/core';
+import { Button, Input, InputHint, HeaderTitleText, SmallTitleText, Icon } from '@renderer/shared/ui';
+import { toAccountId } from '@renderer/shared/lib/utils';
+import { chainsService } from '@renderer/entities/network';
+import { IconNames } from '@renderer/shared/ui/Icon/data';
+import { MultiAccountsList, walletModel } from '@renderer/entities/wallet';
+
+const WalletLogo: Record = {
+ [WalletType.WALLET_CONNECT]: 'walletConnectOnboarding',
+ [WalletType.NOVA_WALLET]: 'novaWalletOnboarding',
+};
+
+type WalletForm = {
+ walletName: string;
+};
+
+type WalletTypeName = WalletType.NOVA_WALLET | WalletType.WALLET_CONNECT;
+
+type Props = {
+ accounts: string[];
+ pairingTopic: string;
+ sessionTopic: string;
+ type: WalletTypeName;
+ onBack: () => void;
+ onComplete: () => void;
+};
+
+const ManageStep = ({ accounts, type, pairingTopic, sessionTopic, onBack, onComplete }: Props) => {
+ const { t } = useI18n();
+ const { showStatus } = useStatusContext();
+
+ const [chains, setChains] = useState([]);
+ const [accountsList, setAccountsList] = useState<{ chain: Chain; accountId: AccountId }[]>([]);
+
+ const {
+ handleSubmit,
+ control,
+ reset,
+ formState: { errors, isValid },
+ } = useForm({
+ mode: 'onChange',
+ defaultValues: { walletName: '' },
+ });
+
+ useEffect(() => {
+ const chains = chainsService.getChainsData();
+ setChains(chainsService.sortChains(chains));
+ }, []);
+
+ useEffect(() => {
+ const list = chains.reduce<{ chain: Chain; accountId: AccountId }[]>((acc, chain) => {
+ const account = accounts.find((account) => {
+ const [_, chainId] = account.split(':');
+
+ return chain.chainId.includes(chainId);
+ });
+
+ const [_, _chainId, address] = account?.split(':') || [];
+
+ if (address) {
+ const accountId = toAccountId(address);
+
+ acc.push({
+ chain,
+ accountId,
+ });
+ }
+
+ return acc;
+ }, []);
+
+ setAccountsList(list.filter(Boolean));
+ }, [chains.length]);
+
+ // TODO: Rewrite with effector forms
+ const submitHandler: SubmitHandler = async ({ walletName }) => {
+ walletModel.events.walletConnectCreated({
+ wallet: {
+ name: walletName.trim(),
+ type,
+ signingType: SigningType.WALLET_CONNECT,
+ },
+ accounts: accounts.map((account) => {
+ const [_, chainId, address] = account.split(':');
+ const chain = chains.find((chain) => chain.chainId.includes(chainId));
+ const accountId = toAccountId(address);
+
+ return {
+ name: walletName.trim(),
+ accountId,
+ type: AccountType.WALLET_CONNECT,
+ chainType: ChainType.SUBSTRATE,
+ chainId: chain?.chainId,
+ signingExtras: {
+ pairingTopic,
+ sessionTopic,
+ },
+ } as Omit, 'walletId'>;
+ }),
+ });
+
+ reset();
+
+ showStatus({
+ title: walletName.trim(),
+ description: t('onboarding.walletConnect.pairedDescription'),
+ content: (
+
+ ),
+ });
+
+ onComplete();
+ };
+
+ const goBack = () => {
+ reset();
+ onBack();
+ };
+
+ const Title = {
+ [WalletType.WALLET_CONNECT]: t('onboarding.walletConnect.title'),
+ [WalletType.NOVA_WALLET]: t('onboarding.novaWallet.title'),
+ };
+
+ return (
+ <>
+
+
{Title[type]}
+
{t('onboarding.walletConnect.manageTitle')}
+
+
+
+
+
+ {t('onboarding.vault.accountsTitle')}
+
+
+ >
+ );
+};
+
+export default ManageStep;
diff --git a/src/renderer/pages/Onboarding/WalletConnect/NovaWallet.tsx b/src/renderer/pages/Onboarding/WalletConnect/NovaWallet.tsx
new file mode 100644
index 0000000000..2489ae24a4
--- /dev/null
+++ b/src/renderer/pages/Onboarding/WalletConnect/NovaWallet.tsx
@@ -0,0 +1,143 @@
+import QRCodeStyling from 'qr-code-styling';
+import { useEffect, useRef, useState } from 'react';
+import { useUnit } from 'effector-react';
+
+import { useI18n } from '@renderer/app/providers';
+import { BaseModal, Button, HeaderTitleText, SmallTitleText } from '@renderer/shared/ui';
+import { Animation } from '@renderer/shared/ui/Animation/Animation';
+import ManageStep from './ManageStep/ManageStep';
+import novawallet_onboarding_tutorial from '@video/novawallet_onboarding_tutorial.mp4';
+import novawallet_onboarding_tutorial_webm from '@video/novawallet_onboarding_tutorial.webm';
+import { usePrevious } from '@renderer/shared/lib/hooks';
+import { getWalletConnectChains, walletConnectModel } from '@renderer/entities/walletConnect';
+import { chainsService } from '@renderer/entities/network';
+import { wcOnboardingModel } from '@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model';
+import { EXPIRE_TIMEOUT, NWQRConfig, Step } from './common/const';
+import { useStatusContext } from '@renderer/app/providers/context/StatusContext';
+import { WalletType } from '@renderer/shared/core';
+
+type Props = {
+ isOpen: boolean;
+ size?: number;
+ onClose: () => void;
+ onComplete: () => void;
+};
+
+const qrCode = new QRCodeStyling(NWQRConfig);
+
+export const NovaWallet = ({ isOpen, onClose, onComplete }: Props) => {
+ const { t } = useI18n();
+ const { showStatus } = useStatusContext();
+
+ const session = useUnit(walletConnectModel.$session);
+ const client = useUnit(walletConnectModel.$client);
+ const pairings = useUnit(walletConnectModel.$pairings);
+ const uri = useUnit(walletConnectModel.$uri);
+ const connect = useUnit(walletConnectModel.events.connect);
+ const disconnect = useUnit(walletConnectModel.events.disconnect);
+ const step = useUnit(wcOnboardingModel.$step);
+ const startOnboarding = useUnit(wcOnboardingModel.events.startOnboarding);
+
+ const previousPairings = usePrevious(pairings);
+
+ const [pairingTopic, setPairingTopic] = useState();
+
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ qrCode.append(ref.current);
+ }
+ }, []);
+
+ useEffect(() => {
+ qrCode.update({
+ data: uri,
+ });
+ }, [uri]);
+
+ useEffect(() => {
+ let timeout: any;
+ if (isOpen) {
+ startOnboarding();
+
+ timeout = setTimeout(onClose, EXPIRE_TIMEOUT);
+ }
+
+ return () => {
+ timeout && clearTimeout(timeout);
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ if (step === Step.REJECT) {
+ showStatus({
+ title: t('onboarding.walletConnect.rejected'),
+ content: ,
+ });
+ onClose();
+ }
+ }, [step]);
+
+ useEffect(() => {
+ if (client && isOpen) {
+ const chains = getWalletConnectChains(chainsService.getChainsData());
+ connect({ chains });
+ }
+ }, [client, isOpen]);
+
+ useEffect(() => {
+ const newPairing = pairings?.find((p) => !previousPairings?.find((pp) => pp.topic === p.topic));
+
+ if (newPairing) {
+ setPairingTopic(newPairing.topic);
+ }
+ }, [pairings.length]);
+
+ return (
+
+ {step === Step.SCAN && qrCode && (
+ <>
+
+
{t('onboarding.novaWallet.title')}
+
{t('onboarding.novaWallet.scanTitle')}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {step === Step.MANAGE && session && pairingTopic && (
+
+ )}
+
+ );
+};
diff --git a/src/renderer/pages/Onboarding/WalletConnect/WalletConnect.tsx b/src/renderer/pages/Onboarding/WalletConnect/WalletConnect.tsx
new file mode 100644
index 0000000000..c6eb18abd8
--- /dev/null
+++ b/src/renderer/pages/Onboarding/WalletConnect/WalletConnect.tsx
@@ -0,0 +1,146 @@
+import QRCodeStyling from 'qr-code-styling';
+import { useEffect, useRef, useState } from 'react';
+import { useUnit } from 'effector-react';
+
+import { useI18n } from '@renderer/app/providers';
+import { BaseModal, Button, HeaderTitleText, SmallTitleText } from '@renderer/shared/ui';
+import { Animation } from '@renderer/shared/ui/Animation/Animation';
+import ManageStep from './ManageStep/ManageStep';
+import novawallet_onboarding_tutorial from '@video/novawallet_onboarding_tutorial.mp4';
+import novawallet_onboarding_tutorial_webm from '@video/novawallet_onboarding_tutorial.webm';
+import { usePrevious } from '@renderer/shared/lib/hooks';
+import { getWalletConnectChains, walletConnectModel } from '@renderer/entities/walletConnect';
+import { chainsService } from '@renderer/entities/network';
+import { wcOnboardingModel } from '@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model';
+import { WCQRConfig, Step, EXPIRE_TIMEOUT } from './common/const';
+import { useStatusContext } from '@renderer/app/providers/context/StatusContext';
+import { WalletType } from '@renderer/shared/core';
+
+type Props = {
+ isOpen: boolean;
+ size?: number;
+ onClose: () => void;
+ onComplete: () => void;
+};
+
+const qrCode = new QRCodeStyling(WCQRConfig);
+
+export const WalletConnect = ({ isOpen, onClose, onComplete }: Props) => {
+ const { t } = useI18n();
+
+ const session = useUnit(walletConnectModel.$session);
+ const client = useUnit(walletConnectModel.$client);
+ const pairings = useUnit(walletConnectModel.$pairings);
+ const uri = useUnit(walletConnectModel.$uri);
+ const connect = useUnit(walletConnectModel.events.connect);
+ const disconnect = useUnit(walletConnectModel.events.disconnect);
+ const step = useUnit(wcOnboardingModel.$step);
+ const startOnboarding = useUnit(wcOnboardingModel.events.startOnboarding);
+
+ const previousPairings = usePrevious(pairings);
+
+ const [pairingTopic, setPairingTopic] = useState();
+
+ const { showStatus } = useStatusContext();
+
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (ref.current) {
+ qrCode.append(ref.current);
+ }
+ }, []);
+
+ useEffect(() => {
+ qrCode.update({
+ data: uri,
+ });
+ }, [uri]);
+
+ useEffect(() => {
+ let timeout: any;
+ if (isOpen) {
+ startOnboarding();
+
+ timeout = setTimeout(onClose, EXPIRE_TIMEOUT);
+ }
+
+ return () => {
+ timeout && clearTimeout(timeout);
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ if (step === Step.REJECT) {
+ showStatus({
+ title: t('onboarding.walletConnect.rejected'),
+ content: ,
+ });
+ onClose();
+ }
+ }, [step]);
+
+ useEffect(() => {
+ if (client && isOpen) {
+ const chains = getWalletConnectChains(chainsService.getChainsData());
+ connect({ chains });
+ }
+ }, [client, isOpen]);
+
+ useEffect(() => {
+ const newPairing = pairings?.find((p) => !previousPairings?.find((pp) => pp.topic === p.topic));
+
+ if (newPairing) {
+ setPairingTopic(newPairing.topic);
+ }
+ }, [pairings.length]);
+
+ return (
+ <>
+
+ {step === Step.SCAN && qrCode && (
+ <>
+
+
{t('onboarding.walletConnect.title')}
+
{t('onboarding.walletConnect.scanTitle')}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {step === Step.MANAGE && session && pairingTopic && (
+
+ )}
+
+ >
+ );
+};
diff --git a/src/renderer/pages/Onboarding/WalletConnect/common/const.ts b/src/renderer/pages/Onboarding/WalletConnect/common/const.ts
new file mode 100644
index 0000000000..7399b88ab0
--- /dev/null
+++ b/src/renderer/pages/Onboarding/WalletConnect/common/const.ts
@@ -0,0 +1,56 @@
+import { Options } from 'qr-code-styling';
+
+import WalletTypeImages from '@renderer/shared/ui/Icon/data/walletType';
+
+export const enum Step {
+ SCAN,
+ MANAGE,
+ REJECT,
+ SUCCESS,
+}
+
+const QrConfig = {
+ width: 300,
+ height: 300,
+ imageOptions: {
+ hideBackgroundDots: true,
+ imageSize: 1,
+ margin: 10,
+ },
+ qrOptions: {
+ typeNumber: 0,
+ mode: 'Byte',
+ errorCorrectionLevel: 'L',
+ },
+ type: 'svg',
+ dotsOptions: {
+ type: 'dots',
+ color: '#ff009d',
+ gradient: {
+ type: 'linear',
+ rotation: 0.7853981633974483,
+ colorStops: [
+ { offset: 0, color: '#3384fe' },
+ { offset: 1, color: '#075dc1' },
+ ],
+ },
+ },
+ backgroundOptions: { color: '#ffffff' },
+ cornersSquareOptions: {
+ type: 'extra-rounded',
+ color: '#000000',
+ },
+ cornersDotOptions: { type: undefined, color: '#000000', gradient: undefined },
+} as Partial;
+
+export const WCQRConfig = {
+ ...QrConfig,
+ image: WalletTypeImages.walletConnectOnboarding.img,
+};
+
+export const NWQRConfig = {
+ ...QrConfig,
+ image: WalletTypeImages.novaWalletOnboarding.img,
+};
+
+export const EXPIRE_TIMEOUT = 5 * 60 * 1000;
diff --git a/src/renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model.ts b/src/renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model.ts
new file mode 100644
index 0000000000..3ef7d53148
--- /dev/null
+++ b/src/renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model.ts
@@ -0,0 +1,27 @@
+import { createEvent, createStore, sample } from 'effector';
+
+import { walletConnectModel } from '@renderer/entities/walletConnect';
+import { Step } from '../common/const';
+
+const startOnboarding = createEvent();
+
+const $step = createStore(Step.SCAN).reset(startOnboarding);
+
+sample({
+ clock: walletConnectModel.events.connected,
+ fn: () => Step.MANAGE,
+ target: $step,
+});
+
+sample({
+ clock: walletConnectModel.events.rejectConnection,
+ fn: () => Step.REJECT,
+ target: $step,
+});
+
+export const wcOnboardingModel = {
+ $step,
+ events: {
+ startOnboarding,
+ },
+};
diff --git a/src/renderer/pages/Onboarding/Welcome/Welcome.tsx b/src/renderer/pages/Onboarding/Welcome/Welcome.tsx
index 1b2c5ba2c5..a4340c69c7 100644
--- a/src/renderer/pages/Onboarding/Welcome/Welcome.tsx
+++ b/src/renderer/pages/Onboarding/Welcome/Welcome.tsx
@@ -47,30 +47,30 @@ export const Welcome = () => {
/>
walletProviderModel.events.walletTypeSet(WalletType.WATCH_ONLY)}
+ title={t('onboarding.welcome.novaWalletTitle')}
+ description={t('onboarding.welcome.novaWalletDescription')}
+ iconName="novaWalletOnboarding"
+ onClick={() => walletProviderModel.events.walletTypeSet(WalletType.NOVA_WALLET)}
/>
walletProviderModel.events.walletTypeSet(WalletType.WALLET_CONNECT)}
/>
walletProviderModel.events.walletTypeSet(WalletType.WATCH_ONLY)}
/>
diff --git a/src/renderer/pages/Onboarding/Welcome/WelcomeCard.tsx b/src/renderer/pages/Onboarding/Welcome/WelcomeCard.tsx
index 8347a074a4..22f5474542 100644
--- a/src/renderer/pages/Onboarding/Welcome/WelcomeCard.tsx
+++ b/src/renderer/pages/Onboarding/Welcome/WelcomeCard.tsx
@@ -24,11 +24,7 @@ export const WelcomeCard = ({ title, description, iconName, disabled, onClick }:
)}
onClick={onClick}
>
-
+
diff --git a/src/renderer/pages/Operations/components/ActionSteps/Submit.tsx b/src/renderer/pages/Operations/components/ActionSteps/Submit.tsx
index 2d0e796957..0d5d79f974 100644
--- a/src/renderer/pages/Operations/components/ActionSteps/Submit.tsx
+++ b/src/renderer/pages/Operations/components/ActionSteps/Submit.tsx
@@ -6,7 +6,7 @@ import { useI18n, useMatrix, useMultisigChainContext } from '@renderer/app/provi
import { useMultisigTx, useMultisigEvent } from '@renderer/entities/multisig';
import { toAccountId } from '@renderer/shared/lib/utils';
import { useToggle } from '@renderer/shared/lib/hooks';
-import { Button } from '@renderer/shared/ui';
+import { Button, StatusModal } from '@renderer/shared/ui';
import type { Account, HexString } from '@renderer/shared/core';
import {
MultisigEvent,
@@ -165,12 +165,8 @@ export const Submit = ({
};
return (
-
+
{errorMessage && }
-
+
);
};
diff --git a/src/renderer/pages/Operations/components/ShortTransactionInfo.test.tsx b/src/renderer/pages/Operations/components/ShortTransactionInfo.test.tsx
index 50b173d3b7..01141d91d8 100644
--- a/src/renderer/pages/Operations/components/ShortTransactionInfo.test.tsx
+++ b/src/renderer/pages/Operations/components/ShortTransactionInfo.test.tsx
@@ -4,6 +4,15 @@ import { TransactionAmount } from './TransactionAmount';
import { Transaction, TransactionType } from '@renderer/entities/transaction';
import { TEST_ADDRESS, TEST_CHAIN_ID } from '@renderer/shared/lib/utils';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
jest.mock('@renderer/app/providers', () => ({
useI18n: jest.fn().mockReturnValue({
t: (key: string) => key,
diff --git a/src/renderer/pages/Operations/components/TransactionTitle/TransactionTitle.test.tsx b/src/renderer/pages/Operations/components/TransactionTitle/TransactionTitle.test.tsx
index 32959a316f..b2f8ee8cd1 100644
--- a/src/renderer/pages/Operations/components/TransactionTitle/TransactionTitle.test.tsx
+++ b/src/renderer/pages/Operations/components/TransactionTitle/TransactionTitle.test.tsx
@@ -4,6 +4,15 @@ import { Transaction, TransactionType } from '@renderer/entities/transaction';
import { TEST_ADDRESS, TEST_CHAIN_ID } from '@renderer/shared/lib/utils';
import { TransactionTitle } from './TransactionTitle';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
jest.mock('@renderer/app/providers', () => ({
useI18n: jest.fn().mockReturnValue({
t: (key: string) => key,
diff --git a/src/renderer/pages/Operations/components/modals/ApproveTx.tsx b/src/renderer/pages/Operations/components/modals/ApproveTx.tsx
index 3aa6baa891..26d841f8b5 100644
--- a/src/renderer/pages/Operations/components/modals/ApproveTx.tsx
+++ b/src/renderer/pages/Operations/components/modals/ApproveTx.tsx
@@ -4,7 +4,7 @@ import { Weight } from '@polkadot/types/interfaces';
import { BN } from '@polkadot/util';
import { useUnit } from 'effector-react';
-import { BaseModal, Button, Icon } from '@renderer/shared/ui';
+import { BaseModal, Button } from '@renderer/shared/ui';
import { useI18n } from '@renderer/app/providers';
import { MultisigTransactionDS } from '@renderer/shared/api/storage';
import { useToggle } from '@renderer/shared/lib/hooks';
@@ -18,8 +18,15 @@ import { SignatorySelectModal } from './SignatorySelectModal';
import { useMultisigEvent } from '@renderer/entities/multisig';
import { Signing } from '@renderer/features/operation';
import { OperationTitle } from '@renderer/components/common';
-import type { Address, HexString, Timepoint, MultisigAccount, Account } from '@renderer/shared/core';
import { walletModel, accountUtils, walletUtils } from '@renderer/entities/wallet';
+import {
+ type Address,
+ type HexString,
+ type Timepoint,
+ type MultisigAccount,
+ type Account,
+ WalletType,
+} from '@renderer/shared/core';
import {
OperationResult,
Transaction,
@@ -30,6 +37,7 @@ import {
isXcmTransaction,
MAX_WEIGHT,
} from '@renderer/entities/transaction';
+import { SignButton } from '@renderer/entities/operation/ui/SignButton';
import { priceProviderModel } from '@renderer/entities/price';
type Props = {
@@ -240,13 +248,12 @@ const ApproveTx = ({ tx, account, connection }: Props) => {
{activeStep === Step.CONFIRMATION && (
<>
- }
+ type={activeWallet?.type || WalletType.SINGLE_PARITY_SIGNER}
onClick={trySetSignerAccount}
- >
- {t('operation.signButton')}
-
+ />
>
)}
diff --git a/src/renderer/pages/Operations/components/modals/RejectTx.tsx b/src/renderer/pages/Operations/components/modals/RejectTx.tsx
index ecd805b226..b6e6f98b21 100644
--- a/src/renderer/pages/Operations/components/modals/RejectTx.tsx
+++ b/src/renderer/pages/Operations/components/modals/RejectTx.tsx
@@ -3,7 +3,7 @@ import { UnsignedTransaction } from '@substrate/txwrapper-polkadot';
import { BN } from '@polkadot/util';
import { useUnit } from 'effector-react';
-import { BaseModal, Button, Icon } from '@renderer/shared/ui';
+import { BaseModal, Button } from '@renderer/shared/ui';
import { useI18n } from '@renderer/app/providers';
import { MultisigTransactionDS } from '@renderer/shared/api/storage';
import { useToggle } from '@renderer/shared/lib/hooks';
@@ -16,9 +16,16 @@ import { Submit } from '../ActionSteps/Submit';
import { Confirmation } from '../ActionSteps/Confirmation';
import { Signing } from '@renderer/features/operation';
import { OperationTitle } from '@renderer/components/common';
-import type { MultisigAccount, Account, Address, HexString, Timepoint } from '@renderer/shared/core';
import { walletModel, walletUtils } from '@renderer/entities/wallet';
import { priceProviderModel } from '@renderer/entities/price';
+import {
+ type MultisigAccount,
+ type Account,
+ type Address,
+ type HexString,
+ type Timepoint,
+ WalletType,
+} from '@renderer/shared/core';
import {
Transaction,
TransactionType,
@@ -27,6 +34,7 @@ import {
validateBalance,
isXcmTransaction,
} from '@renderer/entities/transaction';
+import { SignButton } from '@renderer/entities/operation/ui/SignButton';
type Props = {
tx: MultisigTransactionDS;
@@ -191,13 +199,11 @@ const RejectTx = ({ tx, account, connection }: Props) => {
{activeStep === Step.CONFIRMATION && (
<>
- }
+ type={activeWallet?.type || WalletType.SINGLE_PARITY_SIGNER}
onClick={toggleRejectReasonModal}
- >
- {t('operation.signButton')}
-
+ />
>
)}
{activeStep === Step.SIGNING && rejectTx && connection.api && signAccount && (
diff --git a/src/renderer/pages/Settings/Networks/components/NetworkList/NetworkList.test.tsx b/src/renderer/pages/Settings/Networks/components/NetworkList/NetworkList.test.tsx
index acf8b0fa8a..5aa61d10de 100644
--- a/src/renderer/pages/Settings/Networks/components/NetworkList/NetworkList.test.tsx
+++ b/src/renderer/pages/Settings/Networks/components/NetworkList/NetworkList.test.tsx
@@ -4,6 +4,15 @@ import { ConnectionStatus, ConnectionType } from '@renderer/shared/core';
import { ExtendedChain } from '@renderer/entities/network';
import { NetworkList } from './NetworkList';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('pages/Settings/Networks/NetworkList', () => {
const children = () => 'children';
const networks: ExtendedChain[] = [
diff --git a/src/renderer/pages/Staking/Operations/components/Confirmation/Confirmation.tsx b/src/renderer/pages/Staking/Operations/components/Confirmation/Confirmation.tsx
index eda22d0eed..4ebd0ea57c 100644
--- a/src/renderer/pages/Staking/Operations/components/Confirmation/Confirmation.tsx
+++ b/src/renderer/pages/Staking/Operations/components/Confirmation/Confirmation.tsx
@@ -1,12 +1,13 @@
import { BN, BN_ZERO } from '@polkadot/util';
import { ApiPromise } from '@polkadot/api';
import { PropsWithChildren, useState, useEffect } from 'react';
+import { useUnit } from 'effector-react';
import { Icon, Button, FootnoteText, CaptionText, InputHint } from '@renderer/shared/ui';
import { useI18n } from '@renderer/app/providers';
import { useToggle } from '@renderer/shared/lib/hooks';
import { Validator } from '@renderer/shared/core/types/validator';
-import { AddressWithExplorers, accountUtils } from '@renderer/entities/wallet';
+import { AddressWithExplorers, accountUtils, walletModel } from '@renderer/entities/wallet';
import { AssetBalance } from '@renderer/entities/asset';
import {
MultisigTxInitStatus,
@@ -20,9 +21,10 @@ import ValidatorsModal from '../Modals/ValidatorsModal/ValidatorsModal';
import { DestinationType } from '../../common/types';
import { cnTw } from '@renderer/shared/lib/utils';
import { useMultisigTx } from '@renderer/entities/multisig';
-import { RewardsDestination } from '@renderer/shared/core';
+import { RewardsDestination, WalletType } from '@renderer/shared/core';
import type { Account, Asset, Explorer } from '@renderer/shared/core';
import { AssetFiatBalance } from '@renderer/entities/price/ui/AssetFiatBalance';
+import { SignButton } from '@renderer/entities/operation/ui/SignButton';
const ActionStyle = 'group hover:bg-action-background-hover px-2 py-1 rounded';
@@ -61,6 +63,7 @@ export const Confirmation = ({
const { t } = useI18n();
const { getMultisigTxs } = useMultisigTx({});
const { getTransactionHash } = useTransaction();
+ const activeWallet = useUnit(walletModel.$activeWallet);
const [isAccountsOpen, toggleAccounts] = useToggle();
const [isValidatorsOpen, toggleValidators] = useToggle();
@@ -222,13 +225,11 @@ export const Confirmation = ({
- }
+ type={activeWallet?.type || WalletType.SINGLE_PARITY_SIGNER}
onClick={onResult}
- >
- {t('staking.confirmation.signButton')}
-
+ />
diff --git a/src/renderer/pages/Staking/Overview/Overview.tsx b/src/renderer/pages/Staking/Overview/Overview.tsx
index f586042c92..6a429e89b4 100644
--- a/src/renderer/pages/Staking/Overview/Overview.tsx
+++ b/src/renderer/pages/Staking/Overview/Overview.tsx
@@ -109,8 +109,10 @@ export const Overview = () => {
const isMultisig = walletUtils.isMultisig(activeWallet);
const isSingleShard = walletUtils.isSingleShard(activeWallet);
const isSingleMultishard = walletUtils.isMultiShard(activeWallet) && addresses.length === 1;
+ const isNovaWallet = walletUtils.isNovaWallet(activeWallet);
+ const isWalletConnect = walletUtils.isWalletConnect(activeWallet);
- if (isMultisig || isSingleShard || isSingleMultishard) {
+ if (isMultisig || isSingleShard || isSingleMultishard || isNovaWallet || isWalletConnect) {
setSelectedNominators([addresses[0]]);
} else {
setSelectedNominators([]);
diff --git a/src/renderer/shared/api/storage/service/storageService.ts b/src/renderer/shared/api/storage/service/storageService.ts
index 1d4f7a6f96..8f7f2829b7 100644
--- a/src/renderer/shared/api/storage/service/storageService.ts
+++ b/src/renderer/shared/api/storage/service/storageService.ts
@@ -69,6 +69,20 @@ class StorageService
{
}
}
+ async updateAll(items: Array & { id: K }>): Promise {
+ try {
+ const updates = items.map((item) => {
+ return this.dexieTable.update(item.id, item);
+ });
+
+ return Promise.all(updates);
+ } catch (error) {
+ console.log('Error updating object - ', error);
+
+ return Promise.resolve(undefined);
+ }
+ }
+
delete(id: K): Promise {
return this.dexieTable.delete(id);
}
diff --git a/src/renderer/shared/core/index.ts b/src/renderer/shared/core/index.ts
index 484d8ebc26..7b9dfa963c 100644
--- a/src/renderer/shared/core/index.ts
+++ b/src/renderer/shared/core/index.ts
@@ -10,7 +10,7 @@ export type { Wallet, WalletFamily } from './types/wallet';
export { WalletType, SigningType } from './types/wallet';
export { AccountType, KeyType } from './types/account';
-export type { Account, BaseAccount, ChainAccount, MultisigAccount } from './types/account';
+export type { Account, BaseAccount, ChainAccount, MultisigAccount, WalletConnectAccount } from './types/account';
export { AssetType, StakingType } from './types/asset';
export type { Asset, OrmlExtras, StatemineExtras } from './types/asset';
diff --git a/src/renderer/shared/core/types/account.ts b/src/renderer/shared/core/types/account.ts
index bd2dd8e1ab..0c021c08cc 100644
--- a/src/renderer/shared/core/types/account.ts
+++ b/src/renderer/shared/core/types/account.ts
@@ -42,11 +42,11 @@ export type MultisigAccount = BaseAccount & {
creatorAccountId: AccountId;
};
-// export type WalletConnectAccount = Omit & {
-// chainId: ChainId;
-// };
+export type WalletConnectAccount = Omit & {
+ chainId: ChainId;
+};
-export type Account = BaseAccount | ChainAccount | MultisigAccount;
+export type Account = BaseAccount | ChainAccount | MultisigAccount | WalletConnectAccount;
export const enum AccountType {
BASE = 'base',
@@ -54,7 +54,7 @@ export const enum AccountType {
// SHARDED = 'sharded',
// SHARD = 'shard',
MULTISIG = 'multisig',
- // WALLET_CONNECT = 'wallet_connect',
+ WALLET_CONNECT = 'wallet_connect',
}
export const enum KeyType {
diff --git a/src/renderer/shared/core/types/wallet.ts b/src/renderer/shared/core/types/wallet.ts
index bc18749fda..4993c0d508 100644
--- a/src/renderer/shared/core/types/wallet.ts
+++ b/src/renderer/shared/core/types/wallet.ts
@@ -12,21 +12,26 @@ export const enum WalletType {
WATCH_ONLY = 'wallet_wo',
POLKADOT_VAULT = 'wallet_pv',
MULTISIG = 'wallet_ms',
- // WALLET_CONNECT = 'wallet_wc',
- // NOVA_WALLET = 'wallet_nw',
+ WALLET_CONNECT = 'wallet_wc',
+ NOVA_WALLET = 'wallet_nw',
// Legacy
MULTISHARD_PARITY_SIGNER = 'wallet_mps',
SINGLE_PARITY_SIGNER = 'wallet_sps',
}
-export type WalletFamily = WalletType.POLKADOT_VAULT | WalletType.MULTISIG | WalletType.WATCH_ONLY;
+export type WalletFamily =
+ | WalletType.POLKADOT_VAULT
+ | WalletType.MULTISIG
+ | WalletType.WATCH_ONLY
+ | WalletType.WALLET_CONNECT
+ | WalletType.NOVA_WALLET;
export const enum SigningType {
WATCH_ONLY = 'signing_wo',
PARITY_SIGNER = 'signing_ps',
MULTISIG = 'signing_ms',
// POLKADOT_VAULT = 'signing_pv',
- // WALLET_CONNECT = 'signing_wc',
+ WALLET_CONNECT = 'signing_wc',
// NOVA_WALLET = 'signing_nw',
}
diff --git a/src/renderer/shared/lib/utils/validation.ts b/src/renderer/shared/lib/utils/validation.ts
index 30d3dcf136..f545d94018 100644
--- a/src/renderer/shared/lib/utils/validation.ts
+++ b/src/renderer/shared/lib/utils/validation.ts
@@ -5,4 +5,5 @@ export const enum ValidationErrors {
INSUFFICIENT_BALANCE,
INSUFFICIENT_BALANCE_FOR_FEE,
INVALID_SIGNATURE,
+ EXPIRED,
}
diff --git a/src/renderer/shared/ui/Alert/Alert.test.tsx b/src/renderer/shared/ui/Alert/Alert.test.tsx
index 790fda79c8..6cbaab499c 100644
--- a/src/renderer/shared/ui/Alert/Alert.test.tsx
+++ b/src/renderer/shared/ui/Alert/Alert.test.tsx
@@ -3,8 +3,14 @@ import noop from 'lodash/noop';
import Alert from './Alert';
-// jest.mock('dexie-react-hooks');
-// jest.mock('dexie');
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
describe('ui/Alert', () => {
test('should render title and items', () => {
diff --git a/src/renderer/shared/ui/Animation/Animation.tsx b/src/renderer/shared/ui/Animation/Animation.tsx
index e1bf914233..f4ee8d474f 100644
--- a/src/renderer/shared/ui/Animation/Animation.tsx
+++ b/src/renderer/shared/ui/Animation/Animation.tsx
@@ -1,3 +1,4 @@
+import { useEffect, useState } from 'react';
import { useLottie } from 'lottie-react';
import Animations from './Data';
@@ -5,7 +6,7 @@ import Animations from './Data';
export type AnimationNames = keyof typeof Animations;
export type Props = {
- animation: Object;
+ variant: AnimationNames;
width?: number;
height?: number;
loop?: boolean;
@@ -13,7 +14,9 @@ export type Props = {
className?: string;
};
-export const Animation = ({ animation, width = 80, height = 80, loop = false, autoplay = true, className }: Props) => {
+export const Animation = ({ variant, width = 80, height = 80, loop = false, autoplay = true, className }: Props) => {
+ const [animation, setAnimation] = useState();
+
const defaultOptions = {
loop,
autoplay,
@@ -25,5 +28,11 @@ export const Animation = ({ animation, width = 80, height = 80, loop = false, au
const { View } = useLottie(defaultOptions, { width: width, height: height });
+ useEffect(() => {
+ // using same animation repeatedly without deep clone lead to memory leak
+ // https://github.com/airbnb/lottie-web/issues/1159
+ setAnimation(JSON.parse(JSON.stringify(Animations[variant])));
+ }, [variant]);
+
return {View}
;
};
diff --git a/src/renderer/shared/ui/Buttons/Button/Button.test.tsx b/src/renderer/shared/ui/Buttons/Button/Button.test.tsx
index 08fdcb8254..428333836d 100644
--- a/src/renderer/shared/ui/Buttons/Button/Button.test.tsx
+++ b/src/renderer/shared/ui/Buttons/Button/Button.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Button from './Button';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Buttons/Button', () => {
test('should render component', () => {
render(
diff --git a/src/renderer/shared/ui/Checkbox/Checkbox.test.tsx b/src/renderer/shared/ui/Checkbox/Checkbox.test.tsx
index de0232690d..98d16e931c 100644
--- a/src/renderer/shared/ui/Checkbox/Checkbox.test.tsx
+++ b/src/renderer/shared/ui/Checkbox/Checkbox.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Checkbox from './Checkbox';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Checkbox', () => {
test('should render component', () => {
render(test label);
diff --git a/src/renderer/shared/ui/Countdown/Countdown.test.tsx b/src/renderer/shared/ui/Countdown/Countdown.test.tsx
new file mode 100644
index 0000000000..9d8a01e0b6
--- /dev/null
+++ b/src/renderer/shared/ui/Countdown/Countdown.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@testing-library/react';
+
+import { Countdown } from './Countdown';
+
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
+jest.mock('@renderer/app/providers', () => ({
+ useI18n: jest.fn().mockReturnValue({
+ t: (key: string) => key,
+ }),
+}));
+
+describe('ui/Countdown', () => {
+ test('should render component', () => {
+ render();
+
+ const title = screen.getByText('signing.qrCountdownTitle');
+ expect(title).toBeInTheDocument();
+
+ const timer = screen.getByText('0:10');
+ expect(timer).toBeInTheDocument();
+ });
+});
diff --git a/src/renderer/shared/ui/Countdown/Countdown.tsx b/src/renderer/shared/ui/Countdown/Countdown.tsx
new file mode 100644
index 0000000000..00ea2ce995
--- /dev/null
+++ b/src/renderer/shared/ui/Countdown/Countdown.tsx
@@ -0,0 +1,29 @@
+import { useI18n } from '@renderer/app/providers';
+import { cnTw, secondsToMinutes } from '@renderer/shared/lib/utils';
+import { CaptionText, FootnoteText } from '@renderer/shared/ui';
+
+type Props = {
+ countdown: number;
+ className?: string;
+};
+
+export const Countdown = ({ countdown, className }: Props) => {
+ const { t } = useI18n();
+
+ return (
+
+ {t('signing.qrCountdownTitle')}
+ = 60 ? 'bg-label-background-green' : 'bg-label-background-red'),
+ )}
+ >
+ {/* if qr not loaded yet just show zero */}
+ {secondsToMinutes(countdown)}
+
+
+ );
+};
diff --git a/src/renderer/shared/ui/Counter/Counter.test.tsx b/src/renderer/shared/ui/Counter/Counter.test.tsx
index 8a1d5313d5..c8daa7aeb4 100644
--- a/src/renderer/shared/ui/Counter/Counter.test.tsx
+++ b/src/renderer/shared/ui/Counter/Counter.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Counter from './Counter';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Counter', () => {
test('should render component', () => {
render(25);
diff --git a/src/renderer/shared/ui/Dropdowns/Combobox/Combobox.test.tsx b/src/renderer/shared/ui/Dropdowns/Combobox/Combobox.test.tsx
index 80cee9dbd4..f83666486a 100644
--- a/src/renderer/shared/ui/Dropdowns/Combobox/Combobox.test.tsx
+++ b/src/renderer/shared/ui/Dropdowns/Combobox/Combobox.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Combobox from './Combobox';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Combobox/Combobox', () => {
const options = [
{ id: '0', element: 'label_0', value: '0' },
diff --git a/src/renderer/shared/ui/Dropdowns/DropdownButton/DropdownButton.test.tsx b/src/renderer/shared/ui/Dropdowns/DropdownButton/DropdownButton.test.tsx
index 6bebe23ff5..cb35dde94e 100644
--- a/src/renderer/shared/ui/Dropdowns/DropdownButton/DropdownButton.test.tsx
+++ b/src/renderer/shared/ui/Dropdowns/DropdownButton/DropdownButton.test.tsx
@@ -3,6 +3,15 @@ import noop from 'lodash/noop';
import DropdownButton, { ButtonDropdownOption } from './DropdownButton';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Dropdowns/DropdownButton', () => {
const options: ButtonDropdownOption[] = [
{ id: '0', title: 'label_0', iconName: 'globe', onClick: noop },
diff --git a/src/renderer/shared/ui/Dropdowns/MultiSelect/MultiSelect.test.tsx b/src/renderer/shared/ui/Dropdowns/MultiSelect/MultiSelect.test.tsx
index c04e04d3b0..5f6e8ea509 100644
--- a/src/renderer/shared/ui/Dropdowns/MultiSelect/MultiSelect.test.tsx
+++ b/src/renderer/shared/ui/Dropdowns/MultiSelect/MultiSelect.test.tsx
@@ -2,6 +2,15 @@ import { act, render, screen } from '@testing-library/react';
import MultiSelect from './MultiSelect';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Dropdowns/MultiSelect', () => {
const options = [
{ id: '0', element: 'label_0', value: '0' },
diff --git a/src/renderer/shared/ui/Dropdowns/Select/Select.test.tsx b/src/renderer/shared/ui/Dropdowns/Select/Select.test.tsx
index 5f81f04d84..bb344aa446 100644
--- a/src/renderer/shared/ui/Dropdowns/Select/Select.test.tsx
+++ b/src/renderer/shared/ui/Dropdowns/Select/Select.test.tsx
@@ -2,6 +2,15 @@ import { act, render, screen } from '@testing-library/react';
import Select from './Select';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Dropdowns/Select', () => {
const options = [
{ id: '0', element: 'label_0', value: '0' },
diff --git a/src/renderer/shared/ui/Icon/data/walletType.tsx b/src/renderer/shared/ui/Icon/data/walletType.tsx
index 967435a36d..ccfe981753 100644
--- a/src/renderer/shared/ui/Icon/data/walletType.tsx
+++ b/src/renderer/shared/ui/Icon/data/walletType.tsx
@@ -2,16 +2,24 @@ import ParitySignerBgImg, { ReactComponent as ParitySignerBgSvg } from '@images/
import WatchOnlyBgImg, { ReactComponent as WatchOnlyBgSvg } from '@images/walletTypes/watchOnlyBg.svg';
import ParitySignerImg, { ReactComponent as ParitySignerSvg } from '@images/walletTypes/paritySigner.svg';
import WatchOnlyImg, { ReactComponent as WatchOnlySvg } from '@images/walletTypes/watchOnly.svg';
+import WatchOnlyOnboardingImg, {
+ ReactComponent as WatchOnlyOnboardingSvg,
+} from '@images/walletTypes/watchOnlyOnboardiing.svg';
import MultisigBgImg, { ReactComponent as MultisigBgSvg } from '@images/walletTypes/multisigBg.svg';
import MultisigImg, { ReactComponent as MultisigSvg } from '@images/walletTypes/multisig.svg';
import VaultImg, { ReactComponent as VaultSvg } from '@images/walletTypes/vault.svg';
+import VaultOnboardingImg, { ReactComponent as VaultOnboardingSvg } from '@images/walletTypes/vaultOnboarding.svg';
import MultishardImg, { ReactComponent as MultishardSvg } from '@images/walletTypes/multishard.svg';
import NovaWalletImg, { ReactComponent as NovaWalletSvg } from '@images/walletTypes/novaWallet.svg';
-import WatchOnlyOnboardingImg, {
- ReactComponent as WatchOnlyOnboardingSvg,
-} from '@images/walletTypes/watchOnlyOnboardiing.svg';
import LedgerImg, { ReactComponent as LedgerSvg } from '@images/walletTypes/ledger.svg';
+import LedgerOnboardingImg, { ReactComponent as LedgerOnboardingSvg } from '@images/walletTypes/ledgerOnboarding.svg';
import WalletConnectImg, { ReactComponent as WalletConnectSvg } from '@images/walletTypes/walletConnect.svg';
+import WalletConnectOnboardingImg, {
+ ReactComponent as WalletConnectOnboardingSvg,
+} from '@images/walletTypes/walletConnectOnboarding.svg';
+import NovaWalletOnboardingImg, {
+ ReactComponent as NovaWalletOnboardingSvg,
+} from '@images/walletTypes/novaWalletOnboarding.svg';
const WalletTypeImages = {
paritySigner: { svg: ParitySignerSvg, img: ParitySignerImg },
@@ -22,10 +30,14 @@ const WalletTypeImages = {
multisigBg: { svg: MultisigBgSvg, img: MultisigBgImg },
multisig: { svg: MultisigSvg, img: MultisigImg },
vault: { svg: VaultSvg, img: VaultImg },
+ vaultOnboarding: { svg: VaultOnboardingSvg, img: VaultOnboardingImg },
multishard: { svg: MultishardSvg, img: MultishardImg },
novaWallet: { img: NovaWalletImg, svg: NovaWalletSvg },
+ novaWalletOnboarding: { img: NovaWalletOnboardingImg, svg: NovaWalletOnboardingSvg },
ledger: { img: LedgerImg, svg: LedgerSvg },
+ ledgerOnboarding: { img: LedgerOnboardingImg, svg: LedgerOnboardingSvg },
walletConnect: { img: WalletConnectImg, svg: WalletConnectSvg },
+ walletConnectOnboarding: { img: WalletConnectOnboardingImg, svg: WalletConnectOnboardingSvg },
} as const;
export type WalletImages = keyof typeof WalletTypeImages;
diff --git a/src/renderer/shared/ui/InfoLink/InfoLink.test.tsx b/src/renderer/shared/ui/InfoLink/InfoLink.test.tsx
index 580a346aab..fefd1920fe 100644
--- a/src/renderer/shared/ui/InfoLink/InfoLink.test.tsx
+++ b/src/renderer/shared/ui/InfoLink/InfoLink.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import InfoLink from './InfoLink';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('pages/Settings/InfoLink', () => {
test('should render component', () => {
render(
diff --git a/src/renderer/shared/ui/LabelHelpbox/LabelHelpBox.test.tsx b/src/renderer/shared/ui/LabelHelpbox/LabelHelpBox.test.tsx
index 3e2be11475..f6a6faaa54 100644
--- a/src/renderer/shared/ui/LabelHelpbox/LabelHelpBox.test.tsx
+++ b/src/renderer/shared/ui/LabelHelpbox/LabelHelpBox.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import { LabelHelpBox } from './LabelHelpBox';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/LabelHelpBox', () => {
test('should render component', () => {
const label = 'This is simple content';
diff --git a/src/renderer/shared/ui/Loader/Loader.test.tsx b/src/renderer/shared/ui/Loader/Loader.test.tsx
index 84e9bb8f86..32c0916b6e 100644
--- a/src/renderer/shared/ui/Loader/Loader.test.tsx
+++ b/src/renderer/shared/ui/Loader/Loader.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Loader from './Loader';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Loader', () => {
test('should render component', () => {
render();
diff --git a/src/renderer/shared/ui/Modals/BaseModal/BaseModal.test.tsx b/src/renderer/shared/ui/Modals/BaseModal/BaseModal.test.tsx
index 407c46b6ff..3148b212f9 100644
--- a/src/renderer/shared/ui/Modals/BaseModal/BaseModal.test.tsx
+++ b/src/renderer/shared/ui/Modals/BaseModal/BaseModal.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import BaseModal from './BaseModal';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Modals/BaseModal', () => {
test('should render component', () => {
render(
diff --git a/src/renderer/shared/ui/Modals/ConfirmModal/ConfirmModal.test.tsx b/src/renderer/shared/ui/Modals/ConfirmModal/ConfirmModal.test.tsx
index 5d4ba26a34..56c85b2967 100644
--- a/src/renderer/shared/ui/Modals/ConfirmModal/ConfirmModal.test.tsx
+++ b/src/renderer/shared/ui/Modals/ConfirmModal/ConfirmModal.test.tsx
@@ -3,6 +3,15 @@ import noop from 'lodash/noop';
import ConfirmModal from './ConfirmModal';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Modals/ConfirmModal', () => {
const defaultProps = {
isOpen: true,
diff --git a/src/renderer/shared/ui/Modals/StatusModal/StatusModal.tsx b/src/renderer/shared/ui/Modals/StatusModal/StatusModal.tsx
new file mode 100644
index 0000000000..25dbe7035d
--- /dev/null
+++ b/src/renderer/shared/ui/Modals/StatusModal/StatusModal.tsx
@@ -0,0 +1,59 @@
+import { Fragment, PropsWithChildren, ReactNode } from 'react';
+import { Dialog, Transition } from '@headlessui/react';
+
+import { ModalBackdrop, ModalTransition } from '@renderer/shared/ui/Modals/common';
+import { FootnoteText, SmallTitleText } from '@renderer/shared/ui';
+import { cnTw } from '@renderer/shared/lib/utils';
+
+type Props = {
+ content?: ReactNode;
+ title: string;
+ description?: string;
+ isOpen: boolean;
+ onClose: () => void;
+ className?: string;
+};
+
+export const StatusModal = ({
+ title,
+ description,
+ isOpen,
+ content,
+ className,
+ children,
+ onClose,
+}: PropsWithChildren) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/renderer/shared/ui/PopoverLink/PopoverLink.test.tsx b/src/renderer/shared/ui/PopoverLink/PopoverLink.test.tsx
index 93aa86dc71..554f31a888 100644
--- a/src/renderer/shared/ui/PopoverLink/PopoverLink.test.tsx
+++ b/src/renderer/shared/ui/PopoverLink/PopoverLink.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import PopoverLink from './PopoverLink';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('pages/Settings/PopoverLink', () => {
test('should render component', () => {
render(
diff --git a/src/renderer/shared/ui/Popovers/InfoPopover/InfoPopover.test.tsx b/src/renderer/shared/ui/Popovers/InfoPopover/InfoPopover.test.tsx
index 341b667259..a5ba4bccb9 100644
--- a/src/renderer/shared/ui/Popovers/InfoPopover/InfoPopover.test.tsx
+++ b/src/renderer/shared/ui/Popovers/InfoPopover/InfoPopover.test.tsx
@@ -2,6 +2,15 @@ import { act, render, screen } from '@testing-library/react';
import InfoPopover, { InfoSection } from './InfoPopover';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
const menuLinks = [
{
id: '3',
diff --git a/src/renderer/shared/ui/Popovers/Tooltip/Tooltip.test.tsx b/src/renderer/shared/ui/Popovers/Tooltip/Tooltip.test.tsx
index 8db46d839c..c0d2ff3e04 100644
--- a/src/renderer/shared/ui/Popovers/Tooltip/Tooltip.test.tsx
+++ b/src/renderer/shared/ui/Popovers/Tooltip/Tooltip.test.tsx
@@ -3,6 +3,15 @@ import userEvent from '@testing-library/user-event';
import { Tooltip } from './Tooltip';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Popover', () => {
test('should render component', () => {
render(Hover me);
diff --git a/src/renderer/shared/ui/RadioGroup/RadioGroup.test.tsx b/src/renderer/shared/ui/RadioGroup/RadioGroup.test.tsx
index b82838a4ff..7717123c3a 100644
--- a/src/renderer/shared/ui/RadioGroup/RadioGroup.test.tsx
+++ b/src/renderer/shared/ui/RadioGroup/RadioGroup.test.tsx
@@ -2,6 +2,15 @@ import { act, render, screen } from '@testing-library/react';
import RadioGroup from './RadioGroup';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/RadioGroup', () => {
const options = [
{ id: '1', value: 1, title: 'Test 1' },
diff --git a/src/renderer/shared/ui/StatusLabel/StatusLabel.test.tsx b/src/renderer/shared/ui/StatusLabel/StatusLabel.test.tsx
index 115b38f976..72966e7c21 100644
--- a/src/renderer/shared/ui/StatusLabel/StatusLabel.test.tsx
+++ b/src/renderer/shared/ui/StatusLabel/StatusLabel.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import StatusLabel from './StatusLabel';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/StatusLabel', () => {
test('should render component', () => {
render();
diff --git a/src/renderer/shared/ui/Switch/Switch.test.tsx b/src/renderer/shared/ui/Switch/Switch.test.tsx
index e16975ea09..53fa1d0426 100644
--- a/src/renderer/shared/ui/Switch/Switch.test.tsx
+++ b/src/renderer/shared/ui/Switch/Switch.test.tsx
@@ -2,6 +2,15 @@ import { render, screen } from '@testing-library/react';
import Switch from './Switch';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
describe('ui/Switch', () => {
test('should render component', () => {
render(test label);
diff --git a/src/renderer/shared/ui/Tabs/Tabs.test.tsx b/src/renderer/shared/ui/Tabs/Tabs.test.tsx
index c5d4837d13..5722b88ce9 100644
--- a/src/renderer/shared/ui/Tabs/Tabs.test.tsx
+++ b/src/renderer/shared/ui/Tabs/Tabs.test.tsx
@@ -4,6 +4,15 @@ import userEvent from '@testing-library/user-event';
import { Tabs } from './Tabs';
import { TabItem } from './common/types';
+jest.mock('@renderer/entities/walletConnect', () => ({
+ walletConnectModel: { events: {} },
+ DEFAULT_POLKADOT_METHODS: {},
+ getWalletConnectChains: jest.fn(),
+}));
+jest.mock('@renderer/pages/Onboarding/WalletConnect/model/wc-onboarding-model', () => ({
+ wcOnboardingModel: { events: {} },
+}));
+
const tabItems: TabItem[] = [
{ id: '1', title: 'Tab 1 title', panel: tab 1 content
},
{ id: '2', title: 'Tab 2 title', panel: tab 2 content
},
diff --git a/src/renderer/shared/ui/index.ts b/src/renderer/shared/ui/index.ts
index 2e87620226..1b05adb769 100644
--- a/src/renderer/shared/ui/index.ts
+++ b/src/renderer/shared/ui/index.ts
@@ -8,6 +8,7 @@ import ButtonLink from './Buttons/ButtonLink/ButtonLink';
import ButtonBack from './Buttons/ButtonBack/ButtonBack';
import BaseModal from './Modals/BaseModal/BaseModal';
import ConfirmModal from './Modals/ConfirmModal/ConfirmModal';
+import { StatusModal } from './Modals/StatusModal/StatusModal';
import InfoPopover from './Popovers/InfoPopover/InfoPopover';
import { Popover } from './Popovers/Popover/Popover';
import InfoLink from './InfoLink/InfoLink';
@@ -52,6 +53,7 @@ import Duration from './Duration/Duration';
import Loader from './Loader/Loader';
import DetailRow from './DetailRow/DetailRow';
import { Truncate } from './Truncate/Truncate';
+import { Countdown } from './Countdown/Countdown';
// FIXME: Animation component exported separately.
// Adding them to this file causes to crash all tests which use anything from that file
@@ -71,6 +73,7 @@ export {
IconButton,
BaseModal,
ConfirmModal,
+ StatusModal,
InfoPopover,
MenuPopover,
Popover,
@@ -111,4 +114,5 @@ export {
DetailRow,
Truncate,
MainLayout,
+ Countdown,
};
diff --git a/src/renderer/widgets/CreateWallet/model/wallet-provider-model.ts b/src/renderer/widgets/CreateWallet/model/wallet-provider-model.ts
index a78dd56461..d8c0978987 100644
--- a/src/renderer/widgets/CreateWallet/model/wallet-provider-model.ts
+++ b/src/renderer/widgets/CreateWallet/model/wallet-provider-model.ts
@@ -7,6 +7,7 @@ const walletTypeSet = createEvent();
const modalClosed = createEvent();
const storeCleared = createEvent();
const completed = createEvent();
+const rejected = createEvent();
const $walletType = createStore(null).reset([modalClosed, completed]);
@@ -38,6 +39,7 @@ export const walletProviderModel = {
walletTypeSet,
modalClosed,
completed,
+ rejected,
navigateApiChanged: navigationApi.navigateApiChanged,
},
};
diff --git a/src/renderer/widgets/CreateWallet/ui/CreateWalletProvider.tsx b/src/renderer/widgets/CreateWallet/ui/CreateWalletProvider.tsx
index 5d7bb8266c..6033220b39 100644
--- a/src/renderer/widgets/CreateWallet/ui/CreateWalletProvider.tsx
+++ b/src/renderer/widgets/CreateWallet/ui/CreateWalletProvider.tsx
@@ -5,6 +5,8 @@ import { useNavigate } from 'react-router-dom';
import { walletProviderModel } from '../model/wallet-provider-model';
import WatchOnly from '@renderer/pages/Onboarding/WatchOnly/WatchOnly';
import Vault from '@renderer/pages/Onboarding/Vault/Vault';
+import { NovaWallet } from '@renderer/pages/Onboarding/WalletConnect/NovaWallet';
+import { WalletConnect } from '@renderer/pages/Onboarding/WalletConnect/WalletConnect';
import { MultisigAccount } from './MultisigAccount/MultisigAccount';
import { WalletType } from '@renderer/shared/core';
import { Paths } from '@renderer/shared/routes';
@@ -20,6 +22,8 @@ const WalletModals: Record JSX.Element> = {
[WalletType.MULTISHARD_PARITY_SIGNER]: (props) => ,
[WalletType.SINGLE_PARITY_SIGNER]: (props) => ,
[WalletType.MULTISIG]: (props) => ,
+ [WalletType.WALLET_CONNECT]: (props) => ,
+ [WalletType.NOVA_WALLET]: (props) => ,
};
export const CreateWalletProvider = () => {
diff --git a/src/renderer/widgets/SendAssetModal/ui/components/ActionSteps/Confirmation.tsx b/src/renderer/widgets/SendAssetModal/ui/components/ActionSteps/Confirmation.tsx
index ccf00d4a16..a250e5a7e5 100644
--- a/src/renderer/widgets/SendAssetModal/ui/components/ActionSteps/Confirmation.tsx
+++ b/src/renderer/widgets/SendAssetModal/ui/components/ActionSteps/Confirmation.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { useUnit } from 'effector-react';
import { Transaction, DepositWithLabel, Fee, XcmTypes } from '@renderer/entities/transaction';
import { TransactionAmount } from '@renderer/pages/Operations/components/TransactionAmount';
@@ -7,8 +8,11 @@ import { ExtendedChain } from '@renderer/entities/network';
import { useI18n } from '@renderer/app/providers';
import { XcmFee } from '@renderer/entities/transaction/ui/XcmFee/XcmFee';
import { AssetXCM, XcmConfig } from '@renderer/shared/api/xcm';
+import { SignButton } from '@renderer/entities/operation/ui/SignButton';
+import { WalletType } from '@renderer/shared/core';
import type { Account, MultisigAccount } from '@renderer/shared/core';
import Details from '../Details';
+import { walletModel } from '@renderer/entities/wallet';
type Props = {
transaction: Transaction;
@@ -34,6 +38,7 @@ export const Confirmation = ({
onBack,
}: Props) => {
const { t } = useI18n();
+ const activeWallet = useUnit(walletModel.$activeWallet);
const [feeLoaded, setFeeLoaded] = useState(false);
const isXcmTransfer = XcmTypes.includes(transaction?.type);
@@ -98,9 +103,11 @@ export const Confirmation = ({
{t('operation.goBackButton')}
- } onClick={onResult}>
- {t('operation.signButton')}
-
+
);
diff --git a/src/shared/locale/en.json b/src/shared/locale/en.json
index 61f690ead9..7d50587a2d 100644
--- a/src/shared/locale/en.json
+++ b/src/shared/locale/en.json
@@ -192,6 +192,10 @@
"checkAccountsButton": "Check your accounts",
"confirmAccountsListButton": "Yes, these are my accounts",
"continueButton": "Continue",
+ "novaWallet": {
+ "scanTitle": "Scan with Nova Wallet",
+ "title": "Add wallet by Nova Wallet"
+ },
"paritySigner": {
"accessDeniedDescription": "Please make sure you grant access to camera",
"accessDeniedLabel": "Access denied!",
@@ -252,6 +256,16 @@
"scanTitle": "Scan the account QR code",
"title": "Add wallet by Polkadot Vault"
},
+ "walletConnect": {
+ "accountsTitle": "Manage accounts",
+ "addressColumn": "Address",
+ "manageTitle": "Set up your wallet",
+ "nameColumn": "Account name",
+ "pairedDescription": "Paired",
+ "rejected": "Pairing rejected",
+ "scanTitle": "Scan with your mobile wallet",
+ "title": "Add wallet by Wallet Connect"
+ },
"walletNameExample": "Name examples: Main account, My validator, Dotsama crowdloans, etc",
"walletNameLabel": "Wallet name",
"walletNamePlaceholder": "Enter a name for your wallet",
@@ -336,7 +350,11 @@
"selectAccount": "Select account",
"selectSignatory": "Select signatory",
"selectSignatoryTitle": "Select signatory",
- "signButton": "Sign with Polkadot Vault",
+ "sign": {
+ "novaWallet": "Sign with Nova Wallet",
+ "polkadotVault": "Sign with Polkadot Vault",
+ "walletConnect": "Sign with WalletConnect"
+ },
"signatoriesTitle": "Signatories",
"signing": "{ signed } of { threshold } signed",
"status": {
@@ -348,6 +366,21 @@
},
"successMessage": "Operation Approved",
"successRejectMessage": "Operation Rejected",
+ "walletConnect": {
+ "defaultWalletName": "WalletConnect",
+ "expiredDescription": "The operation isn’t valid anymore",
+ "reconnect": {
+ "cancelButton": "Cancel",
+ "confirmButton": "Reconnect",
+ "connected": "Connected",
+ "description": "You need to reconnect to proceed with signing",
+ "reconnecting": "Waiting for Reconnect...",
+ "title": "{ walletName } is not connected"
+ },
+ "rejected": "Rejected",
+ "signTitle": "Confirm the operation on your { walletName } app",
+ "tryAgainButton": "Try again"
+ },
"xcmFee": "Cross-chain fee"
},
"operations": {
@@ -553,7 +586,7 @@
"metadataPortalLink": "Update metadata in Polkadot Vault",
"parsingCount": "{current} of {total}",
"parsingLabel": "Parsing data",
- "qrCountdownTitle": "QR code is valid for",
+ "qrCountdownTitle": "Time remaining",
"qrNotValid": "QR code isn’t valid anymore",
"scanQrTitle": "Scan the operation QR code with Polkadot Vault",
"scanSignatureTitle": "Scan QR code from Polkadot Vault",
@@ -637,7 +670,6 @@
"queueButton": "Add to operation queue",
"restakeRewards": "Restake rewards",
"rewardsDestinationLabel": "Rewards destination",
- "signButton": "Sign with Polkadot Vault",
"signatoryLabel": "Signatory",
"signer": "Signer",
"submittingOperation": "Submitting operation",
@@ -837,14 +869,18 @@
"wallets": {
"addButtonTitle": "Add",
"addMultisig": "Multisig",
+ "addNovaWallet": "Nova Wallet",
"addPolkadotVault": "Polkadot Vault",
+ "addWalletConnect": "Wallet Connect",
"addWatchOnly": "Watch-only",
"multishardLabel": "Multishard",
"multisigLabel": "Multisig",
+ "novaWalletLabel": "Nova Wallet",
"paritySignerLabel": "Polkadot Vault",
"searchPlaceholder": "Search",
"shards": "Shards",
"title": "Wallets",
+ "walletConnectLabel": "Wallet Connect",
"watchOnlyLabel": "Watch-only"
},
"welcome": {
diff --git a/src/shared/locale/ru.json b/src/shared/locale/ru.json
index 3dea3106fb..d2eab9959b 100644
--- a/src/shared/locale/ru.json
+++ b/src/shared/locale/ru.json
@@ -192,6 +192,10 @@
"checkAccountsButton": "Ваши аккаунты",
"confirmAccountsListButton": "Да, это мои аккаунты",
"continueButton": "Continue",
+ "novaWallet": {
+ "scanTitle": "Scan with Nova Wallet",
+ "title": "Pair Nova Wallet"
+ },
"paritySigner": {
"accessDeniedDescription": "Убедитесь, что доступ к камере разрешен",
"accessDeniedLabel": "Доступ запрещен!",
@@ -252,6 +256,16 @@
"scanTitle": "Scan the account QR code",
"title": "Add wallet by Polkadot Vault"
},
+ "walletConnect": {
+ "accountsTitle": "Manage accounts",
+ "addressColumn": "Address",
+ "manageTitle": "Set up your wallet",
+ "nameColumn": "Account name",
+ "pairedDescription": "Paired",
+ "rejected": "Pairing rejected",
+ "scanTitle": "Scan with your mobile wallet",
+ "title": "Pair via Wallet Connect"
+ },
"walletNameExample": "Например: Основной аккаунт, My validator, Dotsama crowdloans, и т.д.",
"walletNameLabel": "Название кошелька",
"walletNamePlaceholder": "Введите название кошелька",
@@ -336,7 +350,11 @@
"selectAccount": "Select account",
"selectSignatory": "Выберите подписанта",
"selectSignatoryTitle": "Выберите подписанта",
- "signButton": "Подписать в Polkadot Vault",
+ "sign": {
+ "novaWallet": "Sign with Nova Wallet",
+ "polkadotVault": "Sign with Polkadot Vault",
+ "walletConnect": "Sign with WalletConnect"
+ },
"signatoriesTitle": "Подписи",
"signing": "{ signed } из { threshold } подписали",
"status": {
@@ -348,6 +366,21 @@
},
"successMessage": "Operation Approved",
"successRejectMessage": "Операция Отменена",
+ "walletConnect": {
+ "defaultWalletName": "WalletConnect",
+ "expiredDescription": "The operation isn’t valid anymore",
+ "reconnect": {
+ "cancelButton": "Cancel",
+ "confirmButton": "Reconnect",
+ "connected": "Connected",
+ "description": "You need to reconnect to proceed with signing",
+ "reconnecting": "Waiting for Reconnect...",
+ "title": "{ walletName } is not connected"
+ },
+ "rejected": "Rejected",
+ "signTitle": "Confirm the operation on your { walletName } app",
+ "tryAgainButton": "Try again"
+ },
"xcmFee": "Cross-chain fee"
},
"operations": {
@@ -637,7 +670,6 @@
"queueButton": "добавить в очередь",
"restakeRewards": "Вложить награды",
"rewardsDestinationLabel": "Выплата наград",
- "signButton": "Подписать в Polkadot Vault",
"signatoryLabel": "Подписант",
"signer": "Подписант",
"submittingOperation": "Подтверждение операции",
@@ -837,14 +869,18 @@
"wallets": {
"addButtonTitle": "Добавить",
"addMultisig": "Multisig",
+ "addNovaWallet": "Nova Wallet",
"addPolkadotVault": "Polkadot Vault",
+ "addWalletConnect": "Wallet Connect",
"addWatchOnly": "Watch-only",
"multishardLabel": "Multishard",
"multisigLabel": "Multisig",
+ "novaWalletLabel": "Nova Wallet",
"paritySignerLabel": "Polkadot Vault",
"searchPlaceholder": "Поиск",
"shards": "Shards",
"title": "Кошельки",
+ "walletConnectLabel": "Wallet Connect",
"watchOnlyLabel": "Watch-only"
},
"welcome": {