From 4ed1a07b8ca08286778e6853d3f7814d8323887c Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 9 Jul 2024 09:14:11 +0200 Subject: [PATCH] Add secp256k1 algorithm support (#377) * Save the proper algorithm when importing seed in new wallet * Save the proper algorithm when importing seed in wallet * Remove console.log * Add Cypress tests * Fix the name in the checkboxes * Fix the env variable * Update cypress tests --- .github/workflows/integration.yml | 1 + .../cypress/e2e/wallet_management.cy.ts | 53 +++++++++++++++++++ .../CreateNewWallet/CreateNewWallet.tsx | 2 +- .../ImportNewWallet/ImportSeed/ImportSeed.tsx | 35 ++++++++++-- .../pages/ImportSeed/ImportSeed.test.tsx | 2 +- .../pages/ImportSeed/ImportSeed.tsx | 9 ++-- .../ImportSeed/SecretSeed/SecretSeed.test.tsx | 2 +- .../ImportSeed/SecretSeed/SecretSeed.tsx | 34 ++++++++++-- .../contexts/WalletContext/WalletContext.tsx | 21 +++++--- packages/extension/src/types/wallet.types.ts | 3 +- 10 files changed, 138 insertions(+), 24 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 90c991f2c..2eb2999b5 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -78,3 +78,4 @@ jobs: CYPRESS_MNEMONIC: ${{vars.CYPRESS_MNEMONIC}} CYPRESS_PASSWORD: ${{vars.CYPRESS_PASSWORD}} CYPRESS_SEED: ${{vars.CYPRESS_SEED}} + CYPRESS_SEED_SECP256K1: ${{vars.CYPRESS_SEED_SECP256K1}} diff --git a/packages/extension/cypress/e2e/wallet_management.cy.ts b/packages/extension/cypress/e2e/wallet_management.cy.ts index d5f2dc85d..3b3a9d6fa 100644 --- a/packages/extension/cypress/e2e/wallet_management.cy.ts +++ b/packages/extension/cypress/e2e/wallet_management.cy.ts @@ -2,6 +2,7 @@ const PASSWORD = Cypress.env('PASSWORD'); const SEED = Cypress.env('SEED'); +const SEED_SECP256K1 = Cypress.env('SEED_SECP256K1'); const MNEMONIC = Cypress.env('MNEMONIC'); const ERROR_MNEMONIC = 'You need 6 digits'; const URL_WALLET = 'http://localhost:3000/'; @@ -121,6 +122,40 @@ describe('Setup the initial wallet (no previous wallet)', () => { cy.contains('Your gateway to the XRPL').should('be.visible'); }); + it('Import a wallet - Family Seed - secp256k1', () => { + // Go to the import a new wallet page + cy.contains('button', 'Import a wallet').click(); + + // Select import by family seed + cy.contains('button', 'Family Seed').click(); + + // Add the seed to the import + cy.get('input[name="seed"]').type(SEED_SECP256K1); + cy.get('.PrivateSwitchBase-input').click(); + cy.contains('button', 'Next').click(); + + // Set up the proper password + cy.get('input[name="password"]').clear().type(PASSWORD); + cy.get('input[name="confirm-password"]').clear().type(PASSWORD); + cy.contains('button', 'Next').click(); + cy.contains("Woo, you're in!").should('be.visible'); + cy.contains('Follow along with product updates or reach out if you have any questions.').should( + 'be.visible' + ); + + // Redirection to the login page + cy.contains('button', 'Finish').click(); + cy.contains('GemWallet').should('be.visible'); + cy.contains('Your gateway to the XRPL').should('be.visible'); + + // Login + cy.get('input[name="password"]').type(PASSWORD); + cy.contains('button', 'Unlock').click(); + + // Make sure that the right wallet is online + cy.contains('r9M7...4g5C').should('be.visible'); + }); + it('Import a wallet - Mnemonic', () => { // Go to the import a new wallet page cy.contains('button', 'Import a wallet').click(); @@ -307,6 +342,24 @@ describe('Add an additional wallet (with previous wallet)', () => { }); }); + it('Import a wallet - Family Seed - secp256k1', () => { + // Go to the import a new wallet page + cy.contains('button', 'Import a new wallet').click(); + + // Select import by family seed + cy.contains('button', 'Family Seed').click(); + + // Add the seed to the import + cy.get('input[name="seed"]').type(SEED_SECP256K1); + cy.get('.PrivateSwitchBase-input').click(); + cy.contains('button', 'Add Seed').click(); + + // Redirection to the wallets page + cy.contains('Your wallets').should('be.visible'); + cy.get('div[data-testid="wallet-container"]').children().should('have.length', 4); + cy.get('div[data-testid="wallet-container"]').first().contains('r9M7...4g5C'); + }); + it('Add a new wallet - Mnemonic', () => { // Go to the import a new wallet page cy.contains('button', 'Import a new wallet').click(); diff --git a/packages/extension/src/components/pages/AddNewWallet/CreateNewWallet/CreateNewWallet.tsx b/packages/extension/src/components/pages/AddNewWallet/CreateNewWallet/CreateNewWallet.tsx index 2b66a944a..15a16721e 100644 --- a/packages/extension/src/components/pages/AddNewWallet/CreateNewWallet/CreateNewWallet.tsx +++ b/packages/extension/src/components/pages/AddNewWallet/CreateNewWallet/CreateNewWallet.tsx @@ -33,7 +33,7 @@ export const CreateNewWallet: FC = ({ password }) => { useEffect(() => { if (wallet?.seed && activeStep === 2) { try { - importSeed(password, wallet.seed); + importSeed({ password, seed: wallet.seed }); navigate(LIST_WALLETS_PATH); } catch (e) { Sentry.captureException('Cannot save wallet - CreateNewWallet: ' + e); diff --git a/packages/extension/src/components/pages/AddNewWallet/ImportNewWallet/ImportSeed/ImportSeed.tsx b/packages/extension/src/components/pages/AddNewWallet/ImportNewWallet/ImportSeed/ImportSeed.tsx index 0da351901..918cb34f6 100644 --- a/packages/extension/src/components/pages/AddNewWallet/ImportNewWallet/ImportSeed/ImportSeed.tsx +++ b/packages/extension/src/components/pages/AddNewWallet/ImportNewWallet/ImportSeed/ImportSeed.tsx @@ -1,11 +1,13 @@ import { FC, useCallback, useRef, useState } from 'react'; -import { TextField, Typography } from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { Checkbox, FormControlLabel, TextField, Tooltip, Typography } from '@mui/material'; import { useNavigate } from 'react-router-dom'; -import { LIST_WALLETS_PATH } from '../../../../../constants'; +import { LIST_WALLETS_PATH, SECONDARY_GRAY } from '../../../../../constants'; import { useWallet } from '../../../../../contexts'; import { PageWithStepper } from '../../../../templates'; +import { ECDSA } from 'xrpl'; export interface ImportSeedProps { activeStep: number; @@ -17,12 +19,17 @@ export const ImportSeed: FC = ({ activeStep, password, handleBa const navigate = useNavigate(); const { importSeed } = useWallet(); const [seedError, setSeedError] = useState(''); + const [isSecp256k1, setSecp256k1] = useState(false); const seedRef = useRef(null); const handleNext = useCallback(() => { const seedValue = seedRef.current?.value; if (seedValue !== undefined) { - const isValidSeed = importSeed(password, seedValue); + const isValidSeed = importSeed({ + password, + seed: seedValue, + algorithm: isSecp256k1 ? ECDSA.secp256k1 : undefined + }); if (isValidSeed) { navigate(LIST_WALLETS_PATH); } else if (isValidSeed === false) { @@ -33,7 +40,7 @@ export const ImportSeed: FC = ({ activeStep, password, handleBa } else { setSeedError('Your seed is empty'); } - }, [importSeed, navigate, password]); + }, [importSeed, isSecp256k1, navigate, password]); return ( = ({ activeStep, password, handleBa style={{ marginTop: '20px' }} autoComplete="off" /> + setSecp256k1(!isSecp256k1)} + name="setSecp256k1" + color="primary" + style={{ transform: 'scale(0.9)' }} + /> + } + label={ + + Use "secp256k1" algorithm{' '} + + + + + } + style={{ marginTop: '5px' }} + /> ); }; diff --git a/packages/extension/src/components/pages/ImportSeed/ImportSeed.test.tsx b/packages/extension/src/components/pages/ImportSeed/ImportSeed.test.tsx index aafcb041c..57b72cfb0 100644 --- a/packages/extension/src/components/pages/ImportSeed/ImportSeed.test.tsx +++ b/packages/extension/src/components/pages/ImportSeed/ImportSeed.test.tsx @@ -28,7 +28,7 @@ describe('ImportSeed Page', () => { const titleElement = screen.getByRole('heading', { name: 'Secret Seed' }); const subTitleElement = screen.getByRole('heading', { - name: 'Please enter your seed in order to load your wallet to GemWallet.' + name: 'Please enter your seed in order to import your wallet to GemWallet.' }); expect(titleElement).toBeVisible(); expect(subTitleElement).toBeVisible(); diff --git a/packages/extension/src/components/pages/ImportSeed/ImportSeed.tsx b/packages/extension/src/components/pages/ImportSeed/ImportSeed.tsx index 52cd658f7..ad7484e13 100644 --- a/packages/extension/src/components/pages/ImportSeed/ImportSeed.tsx +++ b/packages/extension/src/components/pages/ImportSeed/ImportSeed.tsx @@ -1,6 +1,6 @@ import { FC, useCallback, useState } from 'react'; -import { Wallet } from 'xrpl'; +import { ECDSA, Wallet } from 'xrpl'; import { WalletToSave } from '../../../utils'; import { Congratulations } from '../Congratulations'; @@ -17,11 +17,12 @@ export const ImportSeed: FC = () => { setActiveStep((prevActiveStep) => prevActiveStep - 1); }, []); - const handleSecretSeed = useCallback((seed: string) => { - const wallet = Wallet.fromSeed(seed); + const handleSecretSeed = useCallback((seed: string, algorithm: ECDSA | undefined) => { + const wallet = Wallet.fromSeed(seed, { algorithm }); setWallet({ publicAddress: wallet.address, - seed + seed, + algorithm }); setActiveStep(1); }, []); diff --git a/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.test.tsx b/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.test.tsx index b8e9c90e4..0e9cd8891 100644 --- a/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.test.tsx +++ b/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.test.tsx @@ -21,7 +21,7 @@ describe('ImportSeed - SecretSeed Page', () => { ); const titleElement = screen.getByRole('heading', { name: 'Secret Seed' }); const subTitleElement = screen.getByRole('heading', { - name: 'Please enter your seed in order to load your wallet to GemWallet.' + name: 'Please enter your seed in order to import your wallet to GemWallet.' }); expect(titleElement).toBeVisible(); expect(subTitleElement).toBeVisible(); diff --git a/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.tsx b/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.tsx index 35f04f91f..b467cfa93 100644 --- a/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.tsx +++ b/packages/extension/src/components/pages/ImportSeed/SecretSeed/SecretSeed.tsx @@ -1,29 +1,33 @@ import { FC, useCallback, useState } from 'react'; +import { ECDSA } from 'xrpl'; -import { TextField, Typography } from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { Checkbox, FormControlLabel, TextField, Tooltip, Typography } from '@mui/material'; import { useWallet } from '../../../../contexts'; import { PageWithStepper } from '../../../templates'; +import { SECONDARY_GRAY } from '../../../../constants'; export interface SecretSeedProps { activeStep: number; steps: number; onBack: () => void; - onNext: (seed: string) => void; + onNext: (seed: string, algorithm: ECDSA | undefined) => void; } export const SecretSeed: FC = ({ activeStep, steps, onBack, onNext }) => { const [seedError, setSeedError] = useState(''); + const [isSecp256k1, setSecp256k1] = useState(false); const { isValidSeed } = useWallet(); const handleNext = useCallback(() => { const seedValue = (document.getElementById('seed') as HTMLInputElement).value; if (isValidSeed(seedValue)) { - onNext(seedValue); + onNext(seedValue, isSecp256k1 ? ECDSA.secp256k1 : undefined); } else { setSeedError('Your seed is invalid'); } - }, [isValidSeed, onNext]); + }, [isSecp256k1, isValidSeed, onNext]); return ( = ({ activeStep, steps, onBack, onN Secret Seed - Please enter your seed in order to load your wallet to GemWallet. + Please enter your seed in order to import your wallet to GemWallet. = ({ activeStep, steps, onBack, onN style={{ marginTop: '20px' }} autoComplete="off" /> + setSecp256k1(!isSecp256k1)} + name="setSecp256k1" + color="primary" + style={{ transform: 'scale(0.9)' }} + /> + } + label={ + + Use "secp256k1" algorithm{' '} + + + + + } + style={{ marginTop: '5px' }} + /> ); }; diff --git a/packages/extension/src/contexts/WalletContext/WalletContext.tsx b/packages/extension/src/contexts/WalletContext/WalletContext.tsx index cf1593035..1e25867f8 100644 --- a/packages/extension/src/contexts/WalletContext/WalletContext.tsx +++ b/packages/extension/src/contexts/WalletContext/WalletContext.tsx @@ -2,7 +2,7 @@ import { useContext, useState, createContext, FC, useCallback, useEffect } from import * as Sentry from '@sentry/react'; import { useNavigate } from 'react-router-dom'; -import { Wallet } from 'xrpl'; +import { ECDSA, Wallet } from 'xrpl'; import { GEM_WALLET, @@ -27,13 +27,19 @@ import { saveWallet } from '../../utils'; +type ImportSeedProps = { + password: string; + seed: string; + walletName?: string; + algorithm?: ECDSA; +}; export interface WalletContextType { signIn: (password: string, rememberSession?: boolean) => boolean; signOut: () => void; generateWallet: (walletName?: string) => Wallet; selectWallet: (index: number) => void; isValidSeed: (seed: string) => boolean; - importSeed: (password: string, seed: string, walletName?: string) => boolean | undefined; + importSeed: ({ password, seed, walletName, algorithm }: ImportSeedProps) => boolean | undefined; isValidMnemonic: (mnemonic: string) => boolean; importMnemonic: (password: string, mnemonic: string, walletName?: string) => boolean | undefined; isValidNumbers: (numbers: string[]) => boolean; @@ -90,13 +96,13 @@ const WalletProvider: FC = ({ children }) => { }); } - const _wallets = wallets.map(({ name, publicAddress, seed, mnemonic }) => { + const _wallets = wallets.map(({ name, publicAddress, seed, mnemonic, algorithm }) => { if (seed) { return { name, publicAddress, seed, - wallet: Wallet.fromSeed(seed) + wallet: Wallet.fromSeed(seed, { algorithm }) }; } return { @@ -151,15 +157,16 @@ const WalletProvider: FC = ({ children }) => { * undefined: if wallet is already present */ const importSeed = useCallback( - (password: string, seed: string, walletName?: string) => { + ({ password, seed, walletName, algorithm }: ImportSeedProps) => { try { - const wallet = Wallet.fromSeed(seed); + const wallet = Wallet.fromSeed(seed, { algorithm }); if (wallets.filter((w) => w.publicAddress === wallet.address).length > 0) { return undefined; } const _wallet = { publicAddress: wallet.address, - seed + seed, + algorithm }; saveWallet(_wallet, password); setWallets((wallets) => [ diff --git a/packages/extension/src/types/wallet.types.ts b/packages/extension/src/types/wallet.types.ts index 192dcc125..bcd42f7f3 100644 --- a/packages/extension/src/types/wallet.types.ts +++ b/packages/extension/src/types/wallet.types.ts @@ -1,9 +1,10 @@ -import { Wallet as WalletXRPL } from 'xrpl'; +import { ECDSA, Wallet as WalletXRPL } from 'xrpl'; export interface Wallet { name: string; publicAddress: string; seed?: string; mnemonic?: string; + algorithm?: ECDSA; } export interface WalletLedger extends Wallet {