Skip to content

Commit

Permalink
Add secp256k1 algorithm support (#377)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
FlorianBouron authored Jul 9, 2024
1 parent 9f1c122 commit 4ed1a07
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 24 deletions.
1 change: 1 addition & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
53 changes: 53 additions & 0 deletions packages/extension/cypress/e2e/wallet_management.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const CreateNewWallet: FC<CreateNewWalletProps> = ({ 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,12 +19,17 @@ export const ImportSeed: FC<ImportSeedProps> = ({ activeStep, password, handleBa
const navigate = useNavigate();
const { importSeed } = useWallet();
const [seedError, setSeedError] = useState('');
const [isSecp256k1, setSecp256k1] = useState(false);
const seedRef = useRef<HTMLInputElement | null>(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) {
Expand All @@ -33,7 +40,7 @@ export const ImportSeed: FC<ImportSeedProps> = ({ activeStep, password, handleBa
} else {
setSeedError('Your seed is empty');
}
}, [importSeed, navigate, password]);
}, [importSeed, isSecp256k1, navigate, password]);

return (
<PageWithStepper
Expand Down Expand Up @@ -61,6 +68,26 @@ export const ImportSeed: FC<ImportSeedProps> = ({ activeStep, password, handleBa
style={{ marginTop: '20px' }}
autoComplete="off"
/>
<FormControlLabel
control={
<Checkbox
checked={isSecp256k1}
onChange={() => setSecp256k1(!isSecp256k1)}
name="setSecp256k1"
color="primary"
style={{ transform: 'scale(0.9)' }}
/>
}
label={
<Typography style={{ display: 'flex', fontSize: '0.9rem' }} color={SECONDARY_GRAY}>
Use "secp256k1" algorithm{' '}
<Tooltip title="Note: if you don’t know what it means, you should probably keep it unchecked">
<InfoOutlinedIcon style={{ marginLeft: '5px' }} fontSize="small" />
</Tooltip>
</Typography>
}
style={{ marginTop: '5px' }}
/>
</PageWithStepper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}, []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SecretSeedProps> = ({ 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 (
<PageWithStepper
Expand All @@ -37,7 +41,7 @@ export const SecretSeed: FC<SecretSeedProps> = ({ activeStep, steps, onBack, onN
Secret Seed
</Typography>
<Typography variant="subtitle1" component="h2" style={{ marginTop: '30px' }}>
Please enter your seed in order to load your wallet to GemWallet.
Please enter your seed in order to import your wallet to GemWallet.
</Typography>
<TextField
fullWidth
Expand All @@ -50,6 +54,26 @@ export const SecretSeed: FC<SecretSeedProps> = ({ activeStep, steps, onBack, onN
style={{ marginTop: '20px' }}
autoComplete="off"
/>
<FormControlLabel
control={
<Checkbox
checked={isSecp256k1}
onChange={() => setSecp256k1(!isSecp256k1)}
name="setSecp256k1"
color="primary"
style={{ transform: 'scale(0.9)' }}
/>
}
label={
<Typography style={{ display: 'flex', fontSize: '0.9rem' }} color={SECONDARY_GRAY}>
Use "secp256k1" algorithm{' '}
<Tooltip title="Note: if you don’t know what it means, you should probably keep it unchecked">
<InfoOutlinedIcon style={{ marginLeft: '5px' }} fontSize="small" />
</Tooltip>
</Typography>
}
style={{ marginTop: '5px' }}
/>
</PageWithStepper>
);
};
21 changes: 14 additions & 7 deletions packages/extension/src/contexts/WalletContext/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -90,13 +96,13 @@ const WalletProvider: FC<Props> = ({ 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 {
Expand Down Expand Up @@ -151,15 +157,16 @@ const WalletProvider: FC<Props> = ({ 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) => [
Expand Down
3 changes: 2 additions & 1 deletion packages/extension/src/types/wallet.types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 4ed1a07

Please sign in to comment.