From cdaadbaeec87b313ba5739564affd6ae2253c7ee Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 11 Apr 2024 12:48:30 +0200 Subject: [PATCH 1/2] Allow to use eth private key that starts with 0x --- .changelog/1887.feature.md | 1 + src/app/lib/eth-helpers.ts | 15 +++++++++++---- src/app/state/evmAccounts/index.ts | 4 ++-- src/app/state/paratimes/saga.ts | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 .changelog/1887.feature.md diff --git a/.changelog/1887.feature.md b/.changelog/1887.feature.md new file mode 100644 index 0000000000..b54beb14a8 --- /dev/null +++ b/.changelog/1887.feature.md @@ -0,0 +1 @@ +Allow to use eth private key that starts with 0x diff --git a/src/app/lib/eth-helpers.ts b/src/app/lib/eth-helpers.ts index adb2d9cbf8..f86441415f 100644 --- a/src/app/lib/eth-helpers.ts +++ b/src/app/lib/eth-helpers.ts @@ -1,17 +1,24 @@ import * as oasis from '@oasisprotocol/client' import * as oasisRT from '@oasisprotocol/client-rt' -import { bytesToHex, isValidPrivate, privateToAddress, toChecksumAddress } from '@ethereumjs/util' -export { isValidAddress as isValidEthAddress } from '@ethereumjs/util' +import { + bytesToHex, + isValidPrivate, + privateToAddress, + toChecksumAddress, + stripHexPrefix, +} from '@ethereumjs/util' +export { isValidAddress as isValidEthAddress, stripHexPrefix } from '@ethereumjs/util' export const hexToBuffer = (value: string): Buffer => Buffer.from(value, 'hex') export const isValidEthPrivateKey = (ethPrivateKey: string): boolean => { try { - return isValidPrivate(hexToBuffer(ethPrivateKey)) + return isValidPrivate(hexToBuffer(stripHexPrefix(ethPrivateKey))) } catch { return false } } -export const isValidEthPrivateKeyLength = (ethPrivateKey: string) => ethPrivateKey.length === 64 +export const isValidEthPrivateKeyLength = (ethPrivateKey: string) => + stripHexPrefix(ethPrivateKey).length === 64 export const privateToEthAddress = (ethPrivateKey: string): string => toChecksumAddress(bytesToHex(privateToAddress(hexToBuffer(ethPrivateKey)))) diff --git a/src/app/state/evmAccounts/index.ts b/src/app/state/evmAccounts/index.ts index b665513214..55abcbd146 100644 --- a/src/app/state/evmAccounts/index.ts +++ b/src/app/state/evmAccounts/index.ts @@ -1,7 +1,7 @@ import { PayloadAction } from '@reduxjs/toolkit' import { createSlice } from 'utils/@reduxjs/toolkit' import { EvmAccounts } from './types' -import { privateToEthAddress } from '../../lib/eth-helpers' +import { privateToEthAddress, stripHexPrefix } from '../../lib/eth-helpers' export const initialState: EvmAccounts = {} @@ -10,7 +10,7 @@ export const evmAccountsSlice = createSlice({ initialState, reducers: { add(state, action: PayloadAction<{ ethPrivateKey: string }>) { - const ethAddress = privateToEthAddress(action.payload.ethPrivateKey) + const ethAddress = privateToEthAddress(stripHexPrefix(action.payload.ethPrivateKey)) state[ethAddress] = { ethPrivateKey: action.payload.ethPrivateKey, ethAddress: ethAddress, diff --git a/src/app/state/paratimes/saga.ts b/src/app/state/paratimes/saga.ts index 83165d6d23..b4d29a4286 100644 --- a/src/app/state/paratimes/saga.ts +++ b/src/app/state/paratimes/saga.ts @@ -1,7 +1,7 @@ import { call, put, select, takeLatest } from 'typed-redux-saga' import * as oasis from '@oasisprotocol/client' import { accounts, token } from '@oasisprotocol/client-rt' -import { getEvmBech32Address, privateToEthAddress } from 'app/lib/eth-helpers' +import { getEvmBech32Address, privateToEthAddress, stripHexPrefix } from 'app/lib/eth-helpers' import { submitParaTimeTransaction } from 'app/state/transaction/saga' import { getOasisNic } from 'app/state/network/saga' import { selectSelectedNetwork } from 'app/state/network/selectors' @@ -54,7 +54,7 @@ export function* fetchBalance(oasisAddress: string, paraTime: ParaTime) { export function* fetchBalanceUsingEthPrivateKey() { const { transactionForm } = yield* select(selectParaTimes) try { - const address = privateToEthAddress(transactionForm.ethPrivateKey) + const address = privateToEthAddress(stripHexPrefix(transactionForm.ethPrivateKey)) const oasisAddress = yield* call(getEvmBech32Address, address) yield* call(fetchBalance, oasisAddress, transactionForm.paraTime!) } catch (error: any) { @@ -85,7 +85,7 @@ export function* submitTransaction() { } yield* call(submitParaTimeTransaction, runtime, { amount: transactionForm.amount, - ethPrivateKey: transactionForm.ethPrivateKey, + ethPrivateKey: stripHexPrefix(transactionForm.ethPrivateKey), feeAmount: transactionForm.feeAmount || transactionForm.defaultFeeAmount, feeGas: transactionForm.feeGas, recipient: transactionForm.recipient, From 230864b8533ec351b7b11bb908588031d3f67ef0 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Mon, 15 Apr 2024 08:53:32 +0200 Subject: [PATCH 2/2] Add Playwright tests to eth key validation --- playwright/tests/paraTimes.spec.ts | 38 ++++++++++++++++++++++++++- src/utils/__fixtures__/test-inputs.ts | 11 +++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/playwright/tests/paraTimes.spec.ts b/playwright/tests/paraTimes.spec.ts index aef21603d3..2dd7457f5e 100644 --- a/playwright/tests/paraTimes.spec.ts +++ b/playwright/tests/paraTimes.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { privateKey, privateKeyAddress } from '../../src/utils/__fixtures__/test-inputs' +import { privateKey, privateKeyAddress, ethAccount } from '../../src/utils/__fixtures__/test-inputs' import { fillPrivateKeyWithoutPassword } from '../utils/fillPrivateKey' import { warnSlowApi } from '../utils/warnSlowApi' import { mockApi } from '../utils/mockApi' @@ -33,4 +33,40 @@ test.describe('ParaTimes', () => { await page.getByRole('button', { name: /Next/i }).click() await expect(page.getByPlaceholder('0x...')).toHaveValue('') }) + + test('should validate eth private key', async ({ page }) => { + const validKey = ethAccount.privateKey + const validKeyWithPrefix = `0x${validKey}` + const invalidKey = validKey.replace('c', 'g') + const invalidKeyWithPrefix = `0x${invalidKey}` + + async function testPrivateKeyValidation(key, expected) { + await page.getByPlaceholder('Enter Ethereum-compatible private key').fill(key) + await page.getByRole('button', { name: 'Next' }).click() + await expect(page.getByText(expected)).toBeVisible() + } + + await page.goto('/open-wallet/private-key') + await fillPrivateKeyWithoutPassword(page, { + privateKey: privateKey, + privateKeyAddress: privateKeyAddress, + persistenceCheckboxChecked: false, + persistenceCheckboxDisabled: false, + }) + await page.getByTestId('nav-paratime').click() + await page.getByRole('button', { name: /Withdraw/i }).click() + await page.getByRole('button', { name: 'Select a ParaTime' }).click() + await expect(page.getByRole('listbox')).toBeVisible() + await page.getByRole('listbox').locator('button', { hasText: 'Sapphire' }).click() + await page.getByRole('button', { name: 'Next' }).click() + await page.getByPlaceholder(privateKeyAddress).fill(privateKeyAddress) + // valid eth private keys + await testPrivateKeyValidation(validKey, /enter the amount/) + await page.getByRole('button', { name: 'Back' }).click() + await testPrivateKeyValidation(validKeyWithPrefix, /enter the amount/) + await page.getByRole('button', { name: 'Back' }).click() + // invalid eth private keys + await testPrivateKeyValidation(invalidKey, /private key is invalid/) + await testPrivateKeyValidation(invalidKeyWithPrefix, /private key is invalid/) + }) }) diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts index 7e6a49f192..b7286b4010 100644 --- a/src/utils/__fixtures__/test-inputs.ts +++ b/src/utils/__fixtures__/test-inputs.ts @@ -213,6 +213,11 @@ export const walletExtensionV0PersistedState = { }, } satisfies WalletExtensionV0State +export const ethAccount = { + address: '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18', + privateKey: '6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2', +} + export const walletExtensionV0UnlockedState = { account: { address: 'oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6', @@ -235,9 +240,9 @@ export const walletExtensionV0UnlockedState = { }, }, evmAccounts: { - '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18': { - ethAddress: '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18', - ethPrivateKey: '6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2', + [ethAccount.address]: { + ethAddress: ethAccount.address, + ethPrivateKey: ethAccount.privateKey, }, }, createWallet: { checkbox: false, mnemonic: [] },