diff --git a/.changelog/1923.feature.md b/.changelog/1923.feature.md
new file mode 100644
index 0000000000..b54beb14a8
--- /dev/null
+++ b/.changelog/1923.feature.md
@@ -0,0 +1 @@
+Allow to use eth private key that starts with 0x
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/app/lib/eth-helpers.ts b/src/app/lib/eth-helpers.ts
index adb2d9cbf8..ef80464ba9 100644
--- a/src/app/lib/eth-helpers.ts
+++ b/src/app/lib/eth-helpers.ts
@@ -1,7 +1,7 @@
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'
+export { isValidAddress as isValidEthAddress, stripHexPrefix } from '@ethereumjs/util'
export const hexToBuffer = (value: string): Buffer => Buffer.from(value, 'hex')
export const isValidEthPrivateKey = (ethPrivateKey: string): boolean => {
diff --git a/src/app/pages/ParaTimesPage/TransactionRecipient/__tests__/index.test.tsx b/src/app/pages/ParaTimesPage/TransactionRecipient/__tests__/index.test.tsx
index dfbec81393..797d8a60bc 100644
--- a/src/app/pages/ParaTimesPage/TransactionRecipient/__tests__/index.test.tsx
+++ b/src/app/pages/ParaTimesPage/TransactionRecipient/__tests__/index.test.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import { TransactionTypes } from 'app/state/paratimes/types'
+import { TransactionForm, TransactionTypes } from 'app/state/paratimes/types'
import { useParaTimes, ParaTimesHook } from '../../useParaTimes'
import { useParaTimesNavigation, ParaTimesNavigationHook } from '../../useParaTimesNavigation'
import { TransactionRecipient } from '..'
@@ -25,7 +25,7 @@ describe('', () => {
ticker: 'ROSE',
transactionForm: {
recipient: '',
- ethPrivateKey: '',
+ ethPrivateKeyRaw: '',
},
usesOasisAddress: true,
} as ParaTimesHook
@@ -123,7 +123,7 @@ describe('', () => {
...mockUseParaTimesEVMcResult,
transactionForm: {
...mockUseParaTimesEVMcResult.transactionForm,
- ethPrivateKey: '123',
+ ethPrivateKeyRaw: '123',
},
})
jest.mocked(useParaTimesNavigation).mockReturnValue({
@@ -144,7 +144,7 @@ describe('', () => {
...mockUseParaTimesEVMcResult,
transactionForm: {
...mockUseParaTimesEVMcResult.transactionForm,
- ethPrivateKey: '----------------------------------------------------------------',
+ ethPrivateKeyRaw: '----------------------------------------------------------------',
},
})
jest.mocked(useParaTimesNavigation).mockReturnValue({
@@ -160,13 +160,18 @@ describe('', () => {
})
it('should navigate to amount selection step when address is valid', async () => {
+ const ethPrivateKey = mockUseParaTimesEVMcResult.evmAccounts[0].ethPrivateKey
+ const ethPrivateKeyWith0xPrefix = `0x${ethPrivateKey}`
+ const setTransactionForm = jest.fn()
const navigateToAmount = jest.fn()
jest.mocked(useParaTimes).mockReturnValue({
...mockUseParaTimesResult,
+ setTransactionForm,
transactionForm: {
recipient: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe',
- },
- } as ParaTimesHook)
+ ethPrivateKeyRaw: ethPrivateKeyWith0xPrefix,
+ } as TransactionForm,
+ })
jest.mocked(useParaTimesNavigation).mockReturnValue({
...mockUseParaTimesNavigationResult,
navigateToAmount,
@@ -175,6 +180,11 @@ describe('', () => {
await userEvent.click(screen.getByRole('button', { name: 'Next' }))
+ expect(setTransactionForm).toHaveBeenCalledWith({
+ ethPrivateKey: ethPrivateKey,
+ ethPrivateKeyRaw: ethPrivateKeyWith0xPrefix,
+ recipient: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe',
+ })
expect(navigateToAmount).toHaveBeenCalled()
})
diff --git a/src/app/pages/ParaTimesPage/TransactionRecipient/index.tsx b/src/app/pages/ParaTimesPage/TransactionRecipient/index.tsx
index b17ccac7b1..f304d66e60 100644
--- a/src/app/pages/ParaTimesPage/TransactionRecipient/index.tsx
+++ b/src/app/pages/ParaTimesPage/TransactionRecipient/index.tsx
@@ -13,6 +13,7 @@ import { useParaTimes } from '../useParaTimes'
import { useParaTimesNavigation } from '../useParaTimesNavigation'
import { PasswordField } from 'app/components/PasswordField'
import { preventSavingInputsToUserData } from 'app/lib/preventSavingInputsToUserData'
+import { stripHexPrefix } from '../../../lib/eth-helpers'
export const TransactionRecipient = () => {
const { t } = useTranslation()
@@ -65,13 +66,19 @@ export const TransactionRecipient = () => {
onChange={nextValue =>
setTransactionForm({
...nextValue,
- ethPrivateKey:
- typeof nextValue.ethPrivateKey === 'object'
- ? (nextValue.ethPrivateKey as any).value // from suggestions
- : nextValue.ethPrivateKey,
+ ethPrivateKeyRaw:
+ typeof nextValue.ethPrivateKeyRaw === 'object'
+ ? (nextValue.ethPrivateKeyRaw as any).value // from suggestions
+ : nextValue.ethPrivateKeyRaw,
})
}
- onSubmit={navigateToAmount}
+ onSubmit={formData => {
+ setTransactionForm({
+ ...formData.value,
+ ethPrivateKey: stripHexPrefix(formData.value.ethPrivateKeyRaw),
+ })
+ navigateToAmount()
+ }}
value={transactionForm}
style={{ width: isMobile ? '100%' : '465px' }}
{...preventSavingInputsToUserData}
@@ -79,15 +86,15 @@ export const TransactionRecipient = () => {
{isEvmcParaTime && !isDepositing && (
- !isValidEthPrivateKeyLength(ethPrivateKey)
+ inputElementId="ethPrivateKeyRaw"
+ name="ethPrivateKeyRaw"
+ validate={ethPrivateKeyRaw =>
+ !isValidEthPrivateKeyLength(stripHexPrefix(ethPrivateKeyRaw))
? t(
'paraTimes.validation.invalidEthPrivateKeyLength',
'Private key should be 64 characters long',
)
- : !isValidEthPrivateKey(ethPrivateKey)
+ : !isValidEthPrivateKey(stripHexPrefix(ethPrivateKeyRaw))
? t(
'paraTimes.validation.invalidEthPrivateKey',
'Ethereum-compatible private key is invalid',
@@ -98,7 +105,7 @@ export const TransactionRecipient = () => {
'paraTimes.recipient.ethPrivateKeyPlaceholder',
'Enter Ethereum-compatible private key',
)}
- value={transactionForm.ethPrivateKey}
+ value={transactionForm.ethPrivateKeyRaw}
showTip={t('openWallet.privateKey.showPrivateKey', 'Show private key')}
hideTip={t('openWallet.privateKey.hidePrivateKey', 'Hide private key')}
suggestions={evmAccounts.map(acc => ({ label: acc.ethAddress, value: acc.ethPrivateKey }))}
diff --git a/src/app/state/paratimes/index.ts b/src/app/state/paratimes/index.ts
index 47f378b10a..016fd41bb8 100644
--- a/src/app/state/paratimes/index.ts
+++ b/src/app/state/paratimes/index.ts
@@ -14,6 +14,7 @@ export const initialState: ParaTimesState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
+ ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,
diff --git a/src/app/state/paratimes/types.ts b/src/app/state/paratimes/types.ts
index de03b8e4f0..45dddfd5f5 100644
--- a/src/app/state/paratimes/types.ts
+++ b/src/app/state/paratimes/types.ts
@@ -23,7 +23,10 @@ export interface TransactionForm {
confirmTransferToValidator: boolean
confirmTransferToForeignAccount: boolean
defaultFeeAmount: string
+ // compatible with oasisRT.signatureSecp256k1
ethPrivateKey: string
+ // provided by user and used in form inputs allowing back and forth form navigation
+ ethPrivateKeyRaw: string
feeAmount: string
feeGas: string
paraTime?: ParaTime
diff --git a/src/utils/__fixtures__/test-inputs.ts b/src/utils/__fixtures__/test-inputs.ts
index 7e6a49f192..1eca4d07f0 100644
--- a/src/utils/__fixtures__/test-inputs.ts
+++ b/src/utils/__fixtures__/test-inputs.ts
@@ -80,6 +80,7 @@ export const privateKeyUnlockedState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
+ ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,
@@ -213,6 +214,11 @@ export const walletExtensionV0PersistedState = {
},
} satisfies WalletExtensionV0State
+export const ethAccount = {
+ address: '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18',
+ privateKey: '6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2',
+}
+
export const walletExtensionV0UnlockedState = {
account: {
address: 'oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6',
@@ -235,9 +241,9 @@ export const walletExtensionV0UnlockedState = {
},
},
evmAccounts: {
- '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18': {
- ethAddress: '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18',
- ethPrivateKey: '6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2',
+ [ethAccount.address]: {
+ ethAddress: ethAccount.address,
+ ethPrivateKey: ethAccount.privateKey,
},
},
createWallet: { checkbox: false, mnemonic: [] },
@@ -268,6 +274,7 @@ export const walletExtensionV0UnlockedState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
+ ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,