diff --git a/.changelog/1564.bugfix.md b/.changelog/1564.bugfix.md new file mode 100644 index 000000000..b565d84da --- /dev/null +++ b/.changelog/1564.bugfix.md @@ -0,0 +1 @@ +Retain ETH address in details page for accounts with no EVM transactions diff --git a/playwright/tests/accounts.spec.ts b/playwright/tests/accounts.spec.ts index d92ccf873..caec824b2 100644 --- a/playwright/tests/accounts.spec.ts +++ b/playwright/tests/accounts.spec.ts @@ -15,7 +15,7 @@ async function setup(page: Page) { }, ) await page.route( - '**/v1/sapphire/transactions?limit=10&offset=0&rel=oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', + '**/v1/sapphire/transactions?limit=10&offset=0&rel=0x0000000000000000000000000000000000000000', route => { route.fulfill({ body: JSON.stringify({ @@ -26,7 +26,7 @@ async function setup(page: Page) { }) }, ) - await page.route('**/v1/sapphire/accounts/oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', route => { + await page.route('**/v1/sapphire/accounts/0x0000000000000000000000000000000000000000', route => { route.fulfill({ body: JSON.stringify({ address: 'oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', @@ -47,7 +47,7 @@ async function setup(page: Page) { }) }) await page.route( - '**/v1/sapphire/events?limit=10&offset=0&rel=oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', + '**/v1/sapphire/events?limit=10&offset=0&rel=0x0000000000000000000000000000000000000000', route => { route.fulfill({ body: JSON.stringify({ diff --git a/playwright/tests/getPreciseNumberFormat.spec.ts b/playwright/tests/getPreciseNumberFormat.spec.ts index d051ee601..c358a6f98 100644 --- a/playwright/tests/getPreciseNumberFormat.spec.ts +++ b/playwright/tests/getPreciseNumberFormat.spec.ts @@ -15,7 +15,7 @@ async function setup(page: Page, balance: string, decimals: number) { }, ) - await page.route('**/v1/sapphire/accounts/oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', route => { + await page.route('**/v1/sapphire/accounts/0x0000000000000000000000000000000000000000', route => { route.fulfill({ body: JSON.stringify({ address: 'oasis1qq2v39p9fqk997vk6742axrzqyu9v2ncyuqt8uek', diff --git a/src/app/pages/RuntimeAccountDetailsPage/hook.ts b/src/app/pages/RuntimeAccountDetailsPage/hook.ts index 0dd6694d8..029421625 100644 --- a/src/app/pages/RuntimeAccountDetailsPage/hook.ts +++ b/src/app/pages/RuntimeAccountDetailsPage/hook.ts @@ -8,7 +8,6 @@ import { AppErrors } from '../../../types/errors' import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination' import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE as limit } from '../../config' import { SearchScope } from '../../../types/searchScope' -import { getOasisAddressOrNull } from '../../utils/helpers' export const useAccount = (scope: SearchScope, address: string) => { const { network, layer } = scope @@ -34,19 +33,13 @@ export const useAccountTransactions = (scope: SearchScope, address: string) => { // We should use useGetConsensusTransactions() } - const oasisAddress = getOasisAddressOrNull(address) const query = useGetRuntimeTransactions( network, layer, // This is OK since consensus has been handled separately { limit, offset: offset, - rel: oasisAddress!, - }, - { - query: { - enabled: !!oasisAddress, - }, + rel: address, }, ) const { isFetched, isLoading, data } = query @@ -79,22 +72,12 @@ export const useAccountEvents = (scope: SearchScope, address: string) => { // We should use useGetConsensusEvents() } - const oasisAddress = getOasisAddressOrNull(address) - const query = useGetRuntimeEvents( - network, - layer, - { - limit, - offset: offset, - rel: oasisAddress!, - // TODO: implement filtering for non-transactional events - }, - { - query: { - enabled: !!oasisAddress, - }, - }, - ) + const query = useGetRuntimeEvents(network, layer, { + limit, + offset: offset, + rel: address, + // TODO: implement filtering for non-transactional events + }) const { isFetched, isLoading, isError, data } = query const events = data?.data.events diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 84af94f76..84317e86c 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -16,7 +16,6 @@ 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 { getOasisAddressOrNull } from '../../utils/helpers' export const useTokenInfo = (scope: SearchScope, address: string, enabled = true) => { const { network, layer } = scope @@ -24,7 +23,7 @@ export const useTokenInfo = (scope: SearchScope, address: string, enabled = true // There can be no ERC-20 or ERC-721 tokens on consensus throw AppErrors.UnsupportedLayer } - const query = useGetRuntimeEvmTokensAddress(network, layer, address!, { + const query = useGetRuntimeEvmTokensAddress(network, layer, address, { query: { enabled }, }) const token = query.data?.data @@ -38,19 +37,14 @@ export const useTokenInfo = (scope: SearchScope, address: string, enabled = true } export const useTokenTransfers = (scope: SearchScope, params: { address: string }) => { - const oasisAddress = getOasisAddressOrNull(params.address) - return _useTokenTransfers(scope, oasisAddress ? { rel: oasisAddress } : undefined) + return _useTokenTransfers(scope, { rel: params.address }) } export const useNFTInstanceTransfers = ( scope: SearchScope, params: { nft_id: string; contract_address: string }, ) => { - const oasisAddress = getOasisAddressOrNull(params.contract_address) - return _useTokenTransfers( - scope, - oasisAddress ? { nft_id: params.nft_id, contract_address: oasisAddress } : undefined, - ) + return _useTokenTransfers(scope, { nft_id: params.nft_id, contract_address: params.contract_address }) } export const _useTokenTransfers = (scope: SearchScope, params: undefined | GetRuntimeEventsParams) => { @@ -109,21 +103,10 @@ export const useTokenHolders = (scope: SearchScope, address: string) => { // There are no token holders on the consensus layer. } - const oasisAddress = getOasisAddressOrNull(address) - const query = useGetRuntimeEvmTokensAddressHolders( - network, - layer, - oasisAddress!, - { - limit: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE, - offset: offset, - }, - { - query: { - enabled: !!oasisAddress, - }, - }, - ) + const query = useGetRuntimeEvmTokensAddressHolders(network, layer, address, { + limit: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE, + offset: offset, + }) const { isFetched, isLoading, data } = query @@ -157,21 +140,10 @@ export const useTokenInventory = (scope: SearchScope, address: string) => { throw AppErrors.UnsupportedLayer // There are no tokens on the consensus layer. } - const oasisAddress = getOasisAddressOrNull(address) - const query = useGetRuntimeEvmTokensAddressNfts( - network, - layer, - oasisAddress!, - { - limit: NUMBER_OF_INVENTORY_ITEMS, - offset: offset, - }, - { - query: { - enabled: !!oasisAddress, - }, - }, - ) + const query = useGetRuntimeEvmTokensAddressNfts(network, layer, address, { + limit: NUMBER_OF_INVENTORY_ITEMS, + offset: offset, + }) const { isFetched, isLoading, data } = query const inventory = data?.data.evm_nfts @@ -204,23 +176,11 @@ export const useAccountTokenInventory = (scope: SearchScope, address: string, to // There are no tokens on the consensus layer. } - const oasisAddress = getOasisAddressOrNull(address) - const oasisTokenAddress = getOasisAddressOrNull(tokenAddress) - const query = useGetRuntimeAccountsAddressNfts( - network, - layer, - oasisAddress!, - { - limit: NUMBER_OF_INVENTORY_ITEMS, - offset: offset, - token_address: oasisTokenAddress!, - }, - { - query: { - enabled: !!oasisAddress && !!oasisTokenAddress, - }, - }, - ) + const query = useGetRuntimeAccountsAddressNfts(network, layer, address, { + limit: NUMBER_OF_INVENTORY_ITEMS, + offset: offset, + token_address: tokenAddress, + }) const { isFetched, isLoading, data } = query const inventory = data?.data.evm_nfts @@ -250,12 +210,7 @@ export const useNFTInstance = (scope: SearchScope, address: string, id: string) throw AppErrors.UnsupportedLayer // There are no tokens on the consensus layer. } - const oasisAddress = getOasisAddressOrNull(address) - const query = useGetRuntimeEvmTokensAddressNftsId(network, layer, oasisAddress!, id, { - query: { - enabled: !!oasisAddress, - }, - }) + const query = useGetRuntimeEvmTokensAddressNftsId(network, layer, address, id) const { data, isError, isFetched, isLoading } = query if (isError) { diff --git a/src/app/utils/__tests__/getEthAccountAddress.test.ts b/src/app/utils/__tests__/getEthAccountAddress.test.ts deleted file mode 100644 index 6bd36401a..000000000 --- a/src/app/utils/__tests__/getEthAccountAddress.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getEthAccountAddressFromPreimage, getEthAddressForAccount } from '../helpers' -import { suggestedEmptyAccount, suggestedParsedAccount } from '../test-fixtures' - -describe('getEthAccountAddress', () => { - // TODO: enable when jest fixes "TypeError: Expected Uint8Array" - // https://github.com/facebook/jest/issues/4422 - it.skip('should convert preimage to evm addresses', () => { - expect(getEthAccountAddressFromPreimage(suggestedParsedAccount.address_preimage)).toEqual( - suggestedParsedAccount.address_eth, - ) - }) - - it('should return input address on empty account', () => { - const validEthAddress = suggestedParsedAccount.address_eth - expect(getEthAddressForAccount(suggestedEmptyAccount, validEthAddress)).toBe( - '0xBA504818FdD8D3dBA2Ef8fD9B4F4D5c71aD1d1D3', - ) - }) - - it('should return undefined on empty account with invalid ETH address', () => { - expect(getEthAddressForAccount(suggestedEmptyAccount, '0x0')).toBeUndefined() - }) - - it('should return undefined on empty account with undefined supplied as ETH address', () => { - expect(getEthAddressForAccount(suggestedEmptyAccount, undefined)).toBeUndefined() - }) - - it('should valid address from preimage', () => { - expect(getEthAddressForAccount(suggestedParsedAccount, undefined)).toBe( - suggestedParsedAccount.address_eth, - ) - }) -}) diff --git a/src/app/utils/__tests__/getEthAccountAddressFromPreimage.test.ts b/src/app/utils/__tests__/getEthAccountAddressFromPreimage.test.ts new file mode 100644 index 000000000..d7da68f2e --- /dev/null +++ b/src/app/utils/__tests__/getEthAccountAddressFromPreimage.test.ts @@ -0,0 +1,14 @@ +import { getEthAccountAddressFromPreimage } from '../helpers' +import { suggestedEmptyAccount, suggestedParsedAccount } from '../test-fixtures' + +describe('getEthAccountAddressFromPreimage', () => { + it('should convert preimage to evm addresses', () => { + expect(getEthAccountAddressFromPreimage(suggestedParsedAccount.address_preimage)).toEqual( + suggestedParsedAccount.address_eth, + ) + }) + + it('should return undefined on empty account', () => { + expect(getEthAccountAddressFromPreimage(suggestedEmptyAccount.address_preimage)).toBeUndefined() + }) +}) diff --git a/src/app/utils/helpers.ts b/src/app/utils/helpers.ts index 551c36d6c..c68ef31b9 100644 --- a/src/app/utils/helpers.ts +++ b/src/app/utils/helpers.ts @@ -3,7 +3,7 @@ import { Buffer } from 'buffer' import * as oasis from '@oasisprotocol/client' import * as oasisRT from '@oasisprotocol/client-rt' // eslint-disable-next-line no-restricted-imports -import { AddressPreimage, RuntimeAccount } from '../../oasis-nexus/generated/api' +import { AddressPreimage } from '../../oasis-nexus/generated/api' import { validateMnemonic } from 'bip39' import { sha512_256 } from 'js-sha512' @@ -94,17 +94,6 @@ export function getEthAccountAddressFromPreimage(preimage: AddressPreimage | und return getEthAccountAddressFromBase64(preimage.address_data) } -export function getEthAddressForAccount( - account: RuntimeAccount, - possibleEthAddress?: string, -): string | undefined { - // In case of an empty account - if (account.stats.num_txns <= 0 && possibleEthAddress && isValidEthAddress(possibleEthAddress)) - return possibleEthAddress - - return getEthAccountAddressFromPreimage(account.address_preimage) -} - export function uniq(input: T[] | undefined): T[] { return input === undefined ? [] : [...new Set(input)] } diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 6d85f8b52..c6d0746aa 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -16,7 +16,12 @@ import { RuntimeAccount, RuntimeEventType, } from './generated/api' -import { getAccountSize, getEthAddressForAccount, getOasisAddressOrNull } from '../app/utils/helpers' +import { + getAccountSize, + getEthAccountAddressFromPreimage, + getOasisAddressOrNull, + isValidEthAddress, +} from '../app/utils/helpers' import { getCancelTitle, getParameterChangeTitle, getProposalTitle } from '../app/utils/proposals' import { Network } from '../types/network' import { SearchScope } from '../types/searchScope' @@ -226,6 +231,21 @@ function normalizeSymbol(rawSymbol: string | '' | undefined, scope: SearchScope) return whitelistedTickers.includes(symbol as Ticker) ? symbol : 'n/a' } +/** Returns checksummed maybeMatchingEthAddr if it matches oasisAddress when converted */ +function fallbackEthAddress( + oasisAddress: generated.Address | undefined, + maybeMatchingEthAddr: generated.EthOrOasisAddress | undefined, +): `0x${string}` | undefined { + if ( + oasisAddress && + maybeMatchingEthAddr && + isValidEthAddress(maybeMatchingEthAddr) && + getOasisAddressOrNull(maybeMatchingEthAddr) === oasisAddress + ) { + return toChecksumAddress(maybeMatchingEthAddr) + } +} + export const useGetRuntimeTransactions: typeof generated.useGetRuntimeTransactions = ( network, runtime, @@ -245,6 +265,7 @@ export const useGetRuntimeTransactions: typeof generated.useGetRuntimeTransactio transactions: data.transactions.map(tx => { return { ...tx, + to_eth: tx.to_eth || fallbackEthAddress(tx.to, params?.rel), eth_hash: tx.eth_hash ? `0x${tx.eth_hash}` : undefined, // TODO: Decimals may not be correct, should not depend on ParaTime decimals, but fee_symbol fee: fromBaseUnits(tx.fee, paraTimesConfig[runtime].decimals), @@ -374,7 +395,7 @@ export const useGetConsensusAccountsAddress: typeof generated.useGetConsensusAcc ...options, query: { ...(options?.query ?? {}), - enabled: !!address && (options?.query?.enabled ?? true), + enabled: options?.query?.enabled ?? true, }, request: { ...options?.request, @@ -413,14 +434,11 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount address, options, ) => { - // console.log('Should we get', runtime, '/', address, '?', options?.query?.enabled) - const oasisAddress = getOasisAddressOrNull(address) - - const query = generated.useGetRuntimeAccountsAddress(network, runtime, oasisAddress!, { + const query = generated.useGetRuntimeAccountsAddress(network, runtime, address, { ...options, query: { ...(options?.query ?? {}), - enabled: !!oasisAddress && (options?.query?.enabled ?? true), + enabled: !!address && (options?.query?.enabled ?? true), }, request: { ...options?.request, @@ -430,7 +448,9 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount if (status !== 200) return data return groupAccountTokenBalances({ ...data, - address_eth: getEthAddressForAccount(data, address), + address_eth: + getEthAccountAddressFromPreimage(data.address_preimage) || + fallbackEthAddress(data.address, address), evm_contract: data.evm_contract && { ...data.evm_contract, eth_creation_tx: data.evm_contract.eth_creation_tx @@ -477,6 +497,7 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount const runtimeAccount = query.data?.data // TODO: Remove after account balances on Nexus are in sync with the node + const oasisAddress = getOasisAddressOrNull(address) const rpcAccountBalances = useQuery({ enabled: !!oasisAddress, queryKey: [oasisAddress, network, runtime], @@ -798,13 +819,11 @@ export const useGetRuntimeEvmTokensAddress: typeof generated.useGetRuntimeEvmTok address, options, ) => { - const oasisAddress = getOasisAddressOrNull(address) - - return generated.useGetRuntimeEvmTokensAddress(network, runtime, oasisAddress!, { + return generated.useGetRuntimeEvmTokensAddress(network, runtime, address, { ...options, query: { ...(options?.query ?? {}), - enabled: !!oasisAddress && (options?.query?.enabled ?? true), + enabled: options?.query?.enabled ?? true, }, request: { ...options?.request, @@ -851,44 +870,48 @@ export const useGetRuntimeEvents: typeof generated.useGetRuntimeEvents = ( if (status !== 200) return data return { ...data, - events: data.events.map(event => { - const adjustedHash = event.eth_tx_hash ? `0x${event.eth_tx_hash}` : undefined - if ( - event.type === RuntimeEventType.accountstransfer || - event.type === RuntimeEventType.accountsmint || - event.type === RuntimeEventType.accountsburn || - event.type === RuntimeEventType.consensus_accountsdeposit || - event.type === RuntimeEventType.consensus_accountswithdraw || - event.type === RuntimeEventType.consensus_accountsdelegate || - event.type === RuntimeEventType.consensus_accountsundelegate_done - ) { + events: data.events + .map((event): generated.RuntimeEvent => { return { ...event, - evm_log_params: event.evm_log_params?.map(fixChecksumAddressInEvmEventParam), - eth_tx_hash: adjustedHash, body: { ...event.body, - amount: { - // If denomination="" or missing then use runtime's native. Otherwise unknown (would have to get by token name?). - ...event.body.amount, - Amount: fromBaseUnits(event.body.amount.Amount, paraTimesConfig[runtime].decimals), - Denomination: - event.body.amount.Denomination || - getTokensForScope({ network, layer: runtime })[0].ticker, - }, + owner_eth: event.body?.owner_eth || fallbackEthAddress(event.body.owner, params?.rel), + from_eth: event.body?.from_eth || fallbackEthAddress(event.body.from, params?.rel), + to_eth: event.body?.to_eth || fallbackEthAddress(event.body.to, params?.rel), }, + evm_log_params: event.evm_log_params?.map(fixChecksumAddressInEvmEventParam), + eth_tx_hash: event.eth_tx_hash ? `0x${event.eth_tx_hash}` : undefined, layer: runtime, network, } - } - return { - ...event, - evm_log_params: event.evm_log_params?.map(fixChecksumAddressInEvmEventParam), - eth_tx_hash: adjustedHash, - layer: runtime, - network, - } - }), + }) + .map((event): generated.RuntimeEvent => { + if ( + event.type === RuntimeEventType.accountstransfer || + event.type === RuntimeEventType.accountsmint || + event.type === RuntimeEventType.accountsburn || + event.type === RuntimeEventType.consensus_accountsdeposit || + event.type === RuntimeEventType.consensus_accountswithdraw || + event.type === RuntimeEventType.consensus_accountsdelegate || + event.type === RuntimeEventType.consensus_accountsundelegate_done + // consensus_accountsundelegate_start doesn't contain amount + ) { + return { + ...event, + body: { + ...event.body, + amount: { + // If denomination="" or missing then use runtime's native. Otherwise unknown (would have to get by token name?). + ...event.body.amount, + Amount: fromBaseUnits(event.body.amount.Amount, paraTimesConfig[runtime].decimals), + Denomination: event.body.amount.Denomination || getTokensForScope(event)[0].ticker, + }, + }, + } + } + return event + }), } }, ...arrayify(options?.request?.transformResponse),