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

Simplify converting EVM addresses to oasis1 #1365

Merged
merged 6 commits into from
Apr 5, 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/1365.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Simplify converting EVM addresses to oasis1
23 changes: 4 additions & 19 deletions src/app/components/RuntimeEvents/RuntimeEventDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EvmAbiParam, RuntimeEvent, RuntimeEventType } from '../../../oasis-nexus/api'
import { FC, useEffect, useState } from 'react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { StyledDescriptionList } from '../StyledDescriptionList'
import { useScreenSize } from '../../hooks/useScreensize'
Expand Down Expand Up @@ -90,24 +90,9 @@ const EvmLogRow: FC<{
param: EvmAbiParam
addressSwitchOption: AddressSwitchOption
}> = ({ scope, param, addressSwitchOption }) => {
const [address, setAddress] = useState<string>()

useEffect(() => {
if (param.evm_type !== 'address') {
return
}

const resolveAddresses = async () => {
if (addressSwitchOption === AddressSwitchOption.Oasis) {
const oasisAddress = await getOasisAddress(param.value as string)
setAddress(oasisAddress)
} else {
setAddress(param.value as string)
}
}

resolveAddresses()
}, [param, addressSwitchOption])
const evmAddress = param.evm_type === 'address' ? (param.value as string) : undefined
const oasisAddress = evmAddress ? getOasisAddress(evmAddress) : undefined
const address = addressSwitchOption === AddressSwitchOption.Oasis ? oasisAddress : evmAddress

const getCopyToClipboardValue = () => {
if (address) {
Expand Down
29 changes: 15 additions & 14 deletions src/app/components/Search/search-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom'
import { useSearchParams } from 'react-router-dom'
import {
isValidBlockHeight,
isValidBlockHash,
Expand All @@ -7,9 +7,9 @@ import {
isValidEthAddress,
getEvmBech32Address,
} from '../../utils/helpers'
import { Network } from '../../../types/network'
import { RouteUtils, SpecifiedPerEnabledLayer } from '../../utils/route-utils'
import { AppError, AppErrors } from '../../../types/errors'
import { useNetworkParam } from '../../hooks/useScopeParam'

type LayerSuggestions = {
suggestedBlock: string
Expand Down Expand Up @@ -94,14 +94,21 @@ export const validateAndNormalize = {
return searchTerm.replace(/\s/g, '').toLowerCase()
}
},
evmAccount: (searchTerm: string) => {
evmAccount: (searchTerm: string): string | undefined => {
if (isValidEthAddress(`0x${searchTerm}`)) {
return `0x${searchTerm.toLowerCase()}`
}
if (isValidEthAddress(searchTerm)) {
return searchTerm.toLowerCase()
}
},
evmBech32Account: (searchTerm: string): string | undefined => {
const evmAccount = validateAndNormalize.evmAccount(searchTerm)
if (evmAccount) {
// TODO: remove conversion when API supports querying by EVM address
return getEvmBech32Address(evmAccount)
}
},
evmTokenNameFragment: (searchTerm: string) => {
if (searchTerm?.length >= textSearchMininumLength) {
return searchTerm.toLowerCase()
Expand All @@ -121,27 +128,21 @@ export function isSearchValid(searchTerm: string) {
export const getSearchTermFromRequest = (request: Request) =>
new URL(request.url).searchParams.get('q')?.trim() ?? ''

export const searchParamLoader = async ({ request, params }: LoaderFunctionArgs) => {
const { network } = params
if (!!network && !RouteUtils.getEnabledNetworks().includes(network as Network)) {
export const useParamSearch = () => {
const network = useNetworkParam()

if (!!network && !RouteUtils.getEnabledNetworks().includes(network)) {
throw new AppError(AppErrors.InvalidUrl)
}

const searchTerm = getSearchTermFromRequest(request)
const searchTerm = useSearchParams()[0].get('q')?.trim() ?? ''
const normalized = Object.fromEntries(
lubej marked this conversation as resolved.
Show resolved Hide resolved
Object.entries(validateAndNormalize).map(([key, fn]) => [key, fn(searchTerm)]),
) as { [Key in keyof typeof validateAndNormalize]: string | undefined }
return {
searchTerm,
...normalized,
// TODO: remove conversion when API supports querying by EVM address
// TODO: without async conversion, this won't need to even be a loader
evmBech32Account: normalized.evmAccount ? await getEvmBech32Address(normalized.evmAccount) : undefined,
}
lubej marked this conversation as resolved.
Show resolved Hide resolved
}

export const useParamSearch = () => {
return useLoaderData() as Awaited<ReturnType<typeof searchParamLoader>>
}

export type SearchParams = ReturnType<typeof useParamSearch>
32 changes: 0 additions & 32 deletions src/app/components/Tokens/hooks.ts

This file was deleted.

25 changes: 0 additions & 25 deletions src/app/hooks/useTransformToOasisAddress.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/app/pages/RuntimeAccountDetailsPage/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AppErrors } from '../../../types/errors'
import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination'
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config'
import { SearchScope } from '../../../types/searchScope'
import { useTransformToOasisAddress } from '../../hooks/useTransformToOasisAddress'
import { getOasisAddressOrNull } from '../../utils/helpers'

export const useAccount = (scope: SearchScope, address: string) => {
const { network, layer } = scope
Expand All @@ -34,7 +34,7 @@ export const useAccountTransactions = (scope: SearchScope, address: string) => {
// We should use useGetConsensusTransactions()
}

const oasisAddress = useTransformToOasisAddress(address)
const oasisAddress = getOasisAddressOrNull(address)
const query = useGetRuntimeTransactions(
network,
layer, // This is OK since consensus has been handled separately
Expand Down Expand Up @@ -77,7 +77,7 @@ export const useAccountEvents = (scope: SearchScope, address: string) => {
// We should use useGetConsensusEvents()
}

const oasisAddress = useTransformToOasisAddress(address)
const oasisAddress = getOasisAddressOrNull(address)
const query = useGetRuntimeEvents(
network,
layer,
Expand Down
16 changes: 8 additions & 8 deletions src/app/pages/TokenDashboardPage/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { SearchScope } from '../../../types/searchScope'
import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination'
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config'
import { useComprehensiveSearchParamsPagination } from '../../components/Table/useComprehensiveSearchParamsPagination'
import { useTransformToOasisAddress } from '../../hooks/useTransformToOasisAddress'
import { getOasisAddressOrNull } from '../../utils/helpers'

export const useTokenInfo = (scope: SearchScope, address: string, enabled = true) => {
const { network, layer } = scope
Expand All @@ -38,15 +38,15 @@ export const useTokenInfo = (scope: SearchScope, address: string, enabled = true
}

export const useTokenTransfers = (scope: SearchScope, params: { address: string }) => {
const oasisAddress = useTransformToOasisAddress(params.address)
const oasisAddress = getOasisAddressOrNull(params.address)
return _useTokenTransfers(scope, oasisAddress ? { rel: oasisAddress } : undefined)
}

export const useNFTInstanceTransfers = (
scope: SearchScope,
params: { nft_id: string; contract_address: string },
) => {
const oasisAddress = useTransformToOasisAddress(params.contract_address)
const oasisAddress = getOasisAddressOrNull(params.contract_address)
return _useTokenTransfers(
scope,
oasisAddress ? { nft_id: params.nft_id, contract_address: oasisAddress } : undefined,
Expand Down Expand Up @@ -109,7 +109,7 @@ export const useTokenHolders = (scope: SearchScope, address: string) => {
// There are no token holders on the consensus layer.
}

const oasisAddress = useTransformToOasisAddress(address)
const oasisAddress = getOasisAddressOrNull(address)
const query = useGetRuntimeEvmTokensAddressHolders(
network,
layer,
Expand Down Expand Up @@ -157,7 +157,7 @@ export const useTokenInventory = (scope: SearchScope, address: string) => {
throw AppErrors.UnsupportedLayer
// There are no tokens on the consensus layer.
}
const oasisAddress = useTransformToOasisAddress(address)
const oasisAddress = getOasisAddressOrNull(address)
const query = useGetRuntimeEvmTokensAddressNfts(
network,
layer,
Expand Down Expand Up @@ -204,8 +204,8 @@ export const useAccountTokenInventory = (scope: SearchScope, address: string, to
// There are no tokens on the consensus layer.
}

const oasisAddress = useTransformToOasisAddress(address)
const oasisTokenAddress = useTransformToOasisAddress(tokenAddress)
const oasisAddress = getOasisAddressOrNull(address)
const oasisTokenAddress = getOasisAddressOrNull(tokenAddress)
const query = useGetRuntimeAccountsAddressNfts(
network,
layer,
Expand Down Expand Up @@ -250,7 +250,7 @@ export const useNFTInstance = (scope: SearchScope, address: string, id: string)
throw AppErrors.UnsupportedLayer
// There are no tokens on the consensus layer.
}
const oasisAddress = useTransformToOasisAddress(address)
const oasisAddress = getOasisAddressOrNull(address)
const query = useGetRuntimeEvmTokensAddressNftsId(network, layer, oasisAddress!, id, {
query: {
enabled: !!oasisAddress,
Expand Down
16 changes: 7 additions & 9 deletions src/app/utils/__tests__/getOasisAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@ import { getOasisAddress } from '../helpers'
import { suggestedParsedAccount } from '../test-fixtures'

describe('getOasisAddress', () => {
it('should recognize oasis addresses', async () => {
await expect(getOasisAddress('oasis1qq2vzcvxn0js5unsch5me2xz4kr43vcasv0d5eq4')).resolves.toEqual(
it('should recognize oasis addresses', () => {
expect(getOasisAddress('oasis1qq2vzcvxn0js5unsch5me2xz4kr43vcasv0d5eq4')).toEqual(
'oasis1qq2vzcvxn0js5unsch5me2xz4kr43vcasv0d5eq4',
)
})

it('should convert evm addresses', async () => {
it('should convert evm addresses', () => {
// https://github.com/oasisprotocol/oasis-sdk/blob/22b9e3f972599b56609b7febd9d1ef4cf2437925/client-sdk/go/helpers/address_test.go#L35
await expect(getOasisAddress('0x60a6321eA71d37102Dbf923AAe2E08d005C4e403')).resolves.toEqual(
expect(getOasisAddress('0x60a6321eA71d37102Dbf923AAe2E08d005C4e403')).toEqual(
'oasis1qpaqumrpewltmh9mr73hteycfzveus2rvvn8w5sp',
)

await expect(getOasisAddress(suggestedParsedAccount.address_eth!)).resolves.toEqual(
suggestedParsedAccount.address,
)
expect(getOasisAddress(suggestedParsedAccount.address_eth!)).toEqual(suggestedParsedAccount.address)
})

it('should throw for invalid addresses', async () => {
await expect(getOasisAddress('aaaaaaaaaaaaaaaaaa')).rejects.toThrow()
it('should throw for invalid addresses', () => {
expect(() => getOasisAddress('aaaaaaaaaaaaaaaaaa')).toThrow()
})
})
32 changes: 28 additions & 4 deletions src/app/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as oasisRT from '@oasisprotocol/client-rt'
import { AddressPreimage, RuntimeAccount } from '../../oasis-nexus/generated/api'
import BigNumber from 'bignumber.js'
import { validateMnemonic } from 'bip39'
import { sha512_256 } from 'js-sha512'

export const isValidBlockHeight = (blockHeight: string): boolean => /^[0-9]+$/.test(blockHeight)
export const isValidBlockHash = (hash: string): boolean => /^[0-9a-fA-F]{64}$/.test(hash)
Expand All @@ -26,26 +27,49 @@ export const isValidEthAddress = (hexAddress: string): boolean => {

export const isValidProposalId = (proposalId: string): boolean => /^[0-9]+$/.test(proposalId)

export async function getEvmBech32Address(evmAddress: string) {
/** oasis.address.fromData(...) but without being needlessly asynchronous */
function oasisAddressFromDataSync(
contextIdentifier: string,
contextVersion: number,
data: Uint8Array,
): Uint8Array {
const versionU8 = new Uint8Array([contextVersion])
return oasis.misc.concat(
versionU8,
new Uint8Array(
sha512_256.arrayBuffer(oasis.misc.concat(oasis.misc.fromString(contextIdentifier), versionU8, data)),
).slice(0, 20),
)
}

export function getEvmBech32Address(evmAddress: string) {
const ethAddrU8 = oasis.misc.fromHex(evmAddress.replace('0x', ''))
const addr = await oasis.address.fromData(
const addr = oasisAddressFromDataSync(
oasisRT.address.V0_SECP256K1ETH_CONTEXT_IDENTIFIER,
oasisRT.address.V0_SECP256K1ETH_CONTEXT_VERSION,
ethAddrU8,
)
return oasis.staking.addressToBech32(addr)
}

export const getOasisAddress = async (address: string): Promise<string> => {
export const getOasisAddress = (address: string): string => {
if (isValidOasisAddress(address)) {
return address
} else if (isValidEthAddress(address)) {
return await getEvmBech32Address(address)
return getEvmBech32Address(address)
} else {
throw new Error('Invalid address')
}
}

export const getOasisAddressOrNull = (address: string): string | null => {
try {
return getOasisAddress(address)
} catch (e) {
return null
}
}

export const isValidTxOasisHash = (hash: string): boolean => /^[0-9a-fA-F]{64}$/.test(hash)

export const isValidTxEthHash = (hash: string): boolean => /^0x[0-9a-fA-F]{64}$/.test(hash)
Expand Down
Loading
Loading