Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add secp256k1 algorithm support #377

Merged
merged 7 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions packages/extension/cypress/e2e/wallet_management.cy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/// <reference types="cypress" />

import { XRPLNetwork } from '@gemwallet/constants';

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 +124,56 @@ 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();

// Mainnet should be the default network
cy.get('div[data-testid="network-indicator"]').should('have.text', XRPLNetwork.MAINNET);

// Open the change network window
cy.get('div[data-testid="network-indicator"]').click();
cy.get('div[data-testid="network-indicator-dialog"]', { timeout: 1500 })
.find('header')
.should('have.text', 'Change Network');

// Select the testnet network
cy.contains('button', XRPLNetwork.TESTNET).click();

// Make sure that the network is switched to Testnet
cy.get('div[data-testid="loading"]').should('be.visible');
cy.get('div[data-testid="network-indicator"]').should('have.text', XRPLNetwork.TESTNET);

// Make sure that the right wallet is online
cy.contains('14.999988 XRP').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
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="rememberSession"
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="rememberSession"
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
Loading