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

Allow to use eth private key that starts with 0x - version 2 #1923

Merged
merged 3 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .changelog/1923.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow to use eth private key that starts with 0x
38 changes: 37 additions & 1 deletion playwright/tests/paraTimes.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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/)
})
})
2 changes: 1 addition & 1 deletion src/app/lib/eth-helpers.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 '..'
Expand All @@ -25,7 +25,7 @@ describe('<TransactionRecipient />', () => {
ticker: 'ROSE',
transactionForm: {
recipient: '',
ethPrivateKey: '',
ethPrivateKeyRaw: '',
},
usesOasisAddress: true,
} as ParaTimesHook
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('<TransactionRecipient />', () => {
...mockUseParaTimesEVMcResult,
transactionForm: {
...mockUseParaTimesEVMcResult.transactionForm,
ethPrivateKey: '123',
ethPrivateKeyRaw: '123',
},
})
jest.mocked(useParaTimesNavigation).mockReturnValue({
Expand All @@ -144,7 +144,7 @@ describe('<TransactionRecipient />', () => {
...mockUseParaTimesEVMcResult,
transactionForm: {
...mockUseParaTimesEVMcResult.transactionForm,
ethPrivateKey: '----------------------------------------------------------------',
ethPrivateKeyRaw: '----------------------------------------------------------------',
},
})
jest.mocked(useParaTimesNavigation).mockReturnValue({
Expand All @@ -160,13 +160,18 @@ describe('<TransactionRecipient />', () => {
})

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,
Expand All @@ -175,6 +180,11 @@ describe('<TransactionRecipient />', () => {

await userEvent.click(screen.getByRole('button', { name: 'Next' }))

expect(setTransactionForm).toHaveBeenCalledWith({
ethPrivateKey: ethPrivateKey,
ethPrivateKeyRaw: ethPrivateKeyWith0xPrefix,
recipient: 'oasis1qq3xrq0urs8qcffhvmhfhz4p0mu7ewc8rscnlwxe',
})
expect(navigateToAmount).toHaveBeenCalled()
})

Expand Down
29 changes: 18 additions & 11 deletions src/app/pages/ParaTimesPage/TransactionRecipient/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -65,29 +66,35 @@ 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}
>
<Box margin={{ bottom: 'medium' }}>
{isEvmcParaTime && !isDepositing && (
<PasswordField
inputElementId="ethPrivateKey"
name="ethPrivateKey"
validate={ethPrivateKey =>
!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',
Expand All @@ -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 }))}
Expand Down
1 change: 1 addition & 0 deletions src/app/state/paratimes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const initialState: ParaTimesState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,
Expand Down
3 changes: 3 additions & 0 deletions src/app/state/paratimes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/utils/__fixtures__/test-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const privateKeyUnlockedState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,
Expand Down Expand Up @@ -213,6 +214,11 @@ export const walletExtensionV0PersistedState = {
},
} satisfies WalletExtensionV0State

export const ethAccount = {
address: '0xbA1b346233E5bB5b44f5B4aC6bF224069f427b18',
privateKey: '6593a788d944bb3e25357df140fac5b0e6273f1500a3b37d6513bf9e9807afe2',
}

export const walletExtensionV0UnlockedState = {
account: {
address: 'oasis1qq30ejf9puuc6qnrazmy9dmn7f3gessveum5wnr6',
Expand All @@ -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: [] },
Expand Down Expand Up @@ -268,6 +274,7 @@ export const walletExtensionV0UnlockedState = {
confirmTransferToForeignAccount: false,
defaultFeeAmount: '',
ethPrivateKey: '',
ethPrivateKeyRaw: '',
feeAmount: '',
feeGas: '',
paraTime: undefined,
Expand Down
Loading