From 4f9ff6c42d8ecdd33660310a4b9806358beafd9a Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 15 Dec 2023 12:34:31 +0100 Subject: [PATCH 1/5] Update API bindings --- src/oasis-nexus/generated/api.ts | 45 ++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/oasis-nexus/generated/api.ts b/src/oasis-nexus/generated/api.ts index 1bc1d2312..1eb92dc35 100644 --- a/src/oasis-nexus/generated/api.ts +++ b/src/oasis-nexus/generated/api.ts @@ -208,6 +208,27 @@ Note: The filter will only match on parsed (verified) EVM events. */ evm_log_signature?: string; +/** + * A filter on a smart contract. Every returned event will have been +emitted by the contract at this Oasis address. + + */ +contract_address?: string; +/** + * A filter on NFT events. Every returned event will be specifically +about this NFT instance ID. You must specify the contract_address +filter with this filter. +Currently this only supports ERC-721 Transfer events. +This may expand to support other event types in the future. +If you want only ERC-721 Transfer events, specify +evm_log_signature=ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef +to avoid inadvertently getting other event types if they are +supported later. +Using an evm_log_signature filter with this set to any other value +will match no events. + + */ +nft_id?: string; }; export type GetRuntimeTransactionsParams = { @@ -239,8 +260,8 @@ this account in a way. For example, for an `accounts.Transfer` tx, this will be the sender or the recipient of tokens. Nexus detects related accounts inside EVM transactions and events on a best-effort basis. For example, it inspects ERC20 methods inside `evm.Call` txs. -However, you must provide the oasis-style derived address here, not the Eth address. -See `AddressPreimage` for more info on oasis-style vs Eth addresses. +However, you must provide the Oasis-style derived address here, not the Eth address. +See `AddressPreimage` for more info on Oasis-style vs Eth addresses. */ rel?: string; @@ -659,7 +680,8 @@ Currently only ERC-721 is supported, where the document is an Asset Metadata fro metadata_uri?: string; /** Identifies the asset which this NFT represents */ name?: string; - /** The total number of transfers of this NFT instance. */ + /** The total number of transfers of this NFT instance. + */ num_transfers?: number; /** The Oasis address of this NFT instance's owner. */ owner?: string; @@ -781,7 +803,8 @@ Usually in native denomination, ParaTime units. As a string. body?: RuntimeTransactionBody; /** The fee that was charged for the transaction execution (total, native denomination, ParaTime base units, as a string). -Calculated as `gas_price * gas_used`, where `gas_price = fee / gas_limit`. +For EVM transactions this is calculated as `gas_price * gas_used`, where `gas_price = fee / gas_limit`, for compatibility with Ethereum. +For other transactions this equals to `fee`. */ charged_fee: string; /** The data relevant to the encrypted transaction. Only present for encrypted @@ -1245,7 +1268,7 @@ export interface BareTokenHolder { balance: string; /** The Ethereum address of the same account holder, if meaningfully defined. */ eth_holder_address?: string; - /** The oasis address of the account holder. */ + /** The Oasis address of the account holder. */ holder_address: string; } @@ -1265,7 +1288,7 @@ export type TokenHolderList = List & TokenHolderListAllOf; export interface RuntimeEvmBalance { /** Number of tokens held, in base units. */ balance: string; - /** The oasis address of this token's contract. */ + /** The Oasis address of this token's contract. */ token_contract_addr: string; /** The EVM address of this token's contract. */ token_contract_addr_eth: string; @@ -1306,28 +1329,28 @@ export const AddressDerivationContext = { /** * The data from which a consensus-style address (`oasis1...`) was derived. Notably, for EVM runtimes like Sapphire, -this links the oasis address and the Ethereum address. +this links the Oasis address and the Ethereum address. Oasis addresses are derived from a piece of data, such as an ed25519 public key or an Ethereum address. For example, [this](https://github.com/oasisprotocol/oasis-sdk/blob/b37e6da699df331f5a2ac62793f8be099c68469c/client-sdk/go/helpers/address.go#L90-L91) -is how an Ethereum is converted to an oasis address. The type of underlying data usually also +is how an Ethereum is converted to an Oasis address. The type of underlying data usually also determines how the signatuers for this address are verified. Consensus supports only "staking addresses" (`context="oasis-core/address: staking"` below; always ed25519-backed). Runtimes support all types. This means that every consensus address is also valid in every runtime. For example, in EVM runtimes, you can use staking -addresses, but only with oasis tools (e.g. a wallet); EVM contracts such as +addresses, but only with Oasis tools (e.g. a wallet); EVM contracts such as ERC20 tokens or tools such as Metamask cannot interact with staking addresses. */ export interface AddressPreimage { - /** The base64-encoded data from which the oasis address was derived. + /** The base64-encoded data from which the Oasis address was derived. When `context = "oasis-runtime-sdk/address: secp256k1eth"`, this is the Ethereum address (in base64, not hex!). */ address_data: string; - /** The method by which the oasis address was derived from `address_data`. + /** The method by which the Oasis address was derived from `address_data`. */ context: AddressDerivationContext; /** Version of the `context`. */ From 4ff8e0d04ff086c91880344fe27d6344f4adccd8 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 15 Dec 2023 12:40:27 +0100 Subject: [PATCH 2/5] Create NFT transfers card --- .changelog/1066.feature.md | 1 + .../NFTTokenTransfersCard.tsx | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .changelog/1066.feature.md create mode 100644 src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx diff --git a/.changelog/1066.feature.md b/.changelog/1066.feature.md new file mode 100644 index 000000000..88ed309e9 --- /dev/null +++ b/.changelog/1066.feature.md @@ -0,0 +1 @@ +Add NFT instance token transfers tab diff --git a/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx new file mode 100644 index 000000000..db66c82f3 --- /dev/null +++ b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx @@ -0,0 +1,49 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Card from '@mui/material/Card' +import CardHeader from '@mui/material/CardHeader' +import CardContent from '@mui/material/CardContent' +import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config' +import { ErrorBoundary } from '../../components/ErrorBoundary' +import { TokenTransfers } from '../../components/Tokens/TokenTransfers' +import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' +import { NftDashboardContext } from './' +import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' +import { useTokenTransfers } from '../TokenDashboardPage/hook' + +export const nftTokenTransfersContainerId = 'nftTokenTransfers' + +export const NFTTokenTransfersCard: FC = props => { + const { t } = useTranslation() + + return ( + + + + + + + + + + + ) +} + +const NFTTokenTransfersView: FC = ({ scope, address }) => { + const { t } = useTranslation() + const { isLoading, isFetched, results } = useTokenTransfers(scope, address) + const transfers = results.data + + return ( + <> + {isFetched && !transfers?.length && } + + + ) +} From 4ff48802ccd16bb1f826647fe11b33daa9cd1373 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 15 Dec 2023 12:37:15 +0100 Subject: [PATCH 3/5] Add NFT transfer tab route --- src/app/pages/NFTInstanceDashboardPage/index.tsx | 12 ++++++++++-- src/routes.tsx | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/pages/NFTInstanceDashboardPage/index.tsx b/src/app/pages/NFTInstanceDashboardPage/index.tsx index ac55d7e59..ac5c48e28 100644 --- a/src/app/pages/NFTInstanceDashboardPage/index.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/index.tsx @@ -10,6 +10,7 @@ import { AppErrors } from '../../../types/errors' import { RouterTabs } from 'app/components/RouterTabs' import { SearchScope } from '../../../types/searchScope' import { useNFTInstance } from '../TokenDashboardPage/hook' +import { nftMetadataId } from './NFTMetadataCard' export type NftDashboardContext = { scope: SearchScope @@ -28,7 +29,8 @@ export const NFTInstanceDashboardPage: FC = () => { } const { isFetched, isLoading, nft } = useNFTInstance(scope, address, instanceId) - const metadataLink = useHref('') + const tokenTransfersLink = useHref('') + const metadataLink = useHref(`metadata#${nftMetadataId}`) const context: NftDashboardContext = { scope, address, @@ -45,7 +47,13 @@ export const NFTInstanceDashboardPage: FC = () => { scope={scope} contractAddress={address!} /> - + ) } diff --git a/src/routes.tsx b/src/routes.tsx index b54d6c15b..a445aaa11 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -29,6 +29,7 @@ import { TokenHoldersCard } from './app/pages/TokenDashboardPage/TokenHoldersCar import { TokenInventoryCard } from './app/pages/TokenDashboardPage/TokenInventoryCard' import { NFTInstanceDashboardPage, useNftDetailsProps } from './app/pages/NFTInstanceDashboardPage' import { NFTMetadataCard } from './app/pages/NFTInstanceDashboardPage/NFTMetadataCard' +import { NFTTokenTransfersCard } from './app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard' import { ConsensusDashboardPage } from 'app/pages/ConsensusDashboardPage' import { Layer } from './oasis-nexus/api' import { SearchScope } from './types/searchScope' @@ -158,6 +159,10 @@ export const routes: RouteObject[] = [ children: [ { path: '', + Component: () => , + }, + { + path: 'metadata', Component: () => , }, ], From e7d9d8b989bf7a621017cdb35e7711c9010cec6a Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 15 Dec 2023 12:38:11 +0100 Subject: [PATCH 4/5] Fetch only instance token transfers --- .../NFTTokenTransfersCard.tsx | 4 ++-- src/app/pages/TokenDashboardPage/hook.ts | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx index db66c82f3..fed086562 100644 --- a/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx @@ -30,9 +30,9 @@ export const NFTTokenTransfersCard: FC = props => { ) } -const NFTTokenTransfersView: FC = ({ scope, address }) => { +const NFTTokenTransfersView: FC = ({ scope, address, instanceId }) => { const { t } = useTranslation() - const { isLoading, isFetched, results } = useTokenTransfers(scope, address) + const { isLoading, isFetched, results } = useTokenTransfers(scope, address, instanceId) const transfers = results.data return ( diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 16ee0a3c2..081083b00 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -8,6 +8,7 @@ import { useGetRuntimeEvmTokensAddressNfts, useGetRuntimeAccountsAddressNfts, useGetRuntimeEvmTokensAddressNftsId, + RuntimeEventType, } from '../../../oasis-nexus/api' import { AppErrors } from '../../../types/errors' import { SearchScope } from '../../../types/searchScope' @@ -35,7 +36,7 @@ export const useTokenInfo = (scope: SearchScope, address: string, enabled = true } } -export const useTokenTransfers = (scope: SearchScope, address: string) => { +export const useTokenTransfers = (scope: SearchScope, address: string, nftInstanceId?: string) => { const { network, layer } = scope const pagination = useComprehensiveSearchParamsPagination({ paramName: 'page', @@ -48,15 +49,24 @@ export const useTokenTransfers = (scope: SearchScope, address: string) => { } const oasisAddress = useTransformToOasisAddress(address) + const params = nftInstanceId + ? { + nft_id: nftInstanceId, + contract_address: oasisAddress!, + } + : { + rel: oasisAddress!, + } + const query = useGetRuntimeEvents( network, layer, // This is OK since consensus has been handled separately { ...pagination.paramsForQuery, - rel: oasisAddress!, - type: 'evm.log', + type: RuntimeEventType.evmlog, // The following is the hex-encoded signature for Transfer(address,address,uint256) evm_log_signature: 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + ...params, }, { query: { From 206e3d5fb60b93e11dbe7c9fdb372bd01a193fd6 Mon Sep 17 00:00:00 2001 From: Luka Jeran Date: Wed, 24 Jan 2024 10:49:42 +0100 Subject: [PATCH 5/5] Refactor useTokenTransfers function signature --- .../AccountTokenTransfersCard.tsx | 2 +- .../NFTTokenTransfersCard.tsx | 7 ++-- .../TokenDashboardPage/TokenTransfersCard.tsx | 2 +- src/app/pages/TokenDashboardPage/hook.ts | 36 ++++++++++++------- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountTokenTransfersCard.tsx b/src/app/pages/AccountDetailsPage/AccountTokenTransfersCard.tsx index bb4d4bdf8..1e49ad590 100644 --- a/src/app/pages/AccountDetailsPage/AccountTokenTransfersCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountTokenTransfersCard.tsx @@ -31,7 +31,7 @@ export const AccountTokenTransfersCard: FC = ({ scope, ad const AccountTokenTransfers: FC = ({ scope, address, account }) => { const { t } = useTranslation() - const { isLoading, isFetched, results } = useTokenTransfers(scope, address) + const { isLoading, isFetched, results } = useTokenTransfers(scope, { address }) const transfers = results.data diff --git a/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx index fed086562..b76e82680 100644 --- a/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/NFTTokenTransfersCard.tsx @@ -9,7 +9,7 @@ import { TokenTransfers } from '../../components/Tokens/TokenTransfers' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { NftDashboardContext } from './' import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' -import { useTokenTransfers } from '../TokenDashboardPage/hook' +import { useNFTInstanceTransfers } from '../TokenDashboardPage/hook' export const nftTokenTransfersContainerId = 'nftTokenTransfers' @@ -32,7 +32,10 @@ export const NFTTokenTransfersCard: FC = props => { const NFTTokenTransfersView: FC = ({ scope, address, instanceId }) => { const { t } = useTranslation() - const { isLoading, isFetched, results } = useTokenTransfers(scope, address, instanceId) + const { isLoading, isFetched, results } = useNFTInstanceTransfers(scope, { + contract_address: address, + nft_id: instanceId, + }) const transfers = results.data return ( diff --git a/src/app/pages/TokenDashboardPage/TokenTransfersCard.tsx b/src/app/pages/TokenDashboardPage/TokenTransfersCard.tsx index b89ff76ed..629be0d4e 100644 --- a/src/app/pages/TokenDashboardPage/TokenTransfersCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenTransfersCard.tsx @@ -34,7 +34,7 @@ export const TokenTransfersCard: FC = ({ scope, address } const TokenTransfersView: FC = ({ scope, address }) => { const { t } = useTranslation() - const { isLoading: areTransfersLoading, isFetched, results } = useTokenTransfers(scope, address) + const { isLoading: areTransfersLoading, isFetched, results } = useTokenTransfers(scope, { address }) const { isLoading: isTokenLoading } = useTokenInfo(scope, address) diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 081083b00..b341629d6 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -9,6 +9,7 @@ import { useGetRuntimeAccountsAddressNfts, useGetRuntimeEvmTokensAddressNftsId, RuntimeEventType, + GetRuntimeEventsParams, } from '../../../oasis-nexus/api' import { AppErrors } from '../../../types/errors' import { SearchScope } from '../../../types/searchScope' @@ -36,7 +37,27 @@ export const useTokenInfo = (scope: SearchScope, address: string, enabled = true } } -export const useTokenTransfers = (scope: SearchScope, address: string, nftInstanceId?: string) => { +export const useTokenTransfers = (scope: SearchScope, params: { address: string }) => { + const oasisAddress = useTransformToOasisAddress(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) + return _useTokenTransfers( + scope, + oasisAddress ? { nft_id: params.nft_id, contract_address: oasisAddress } : undefined, + ) +} + +export const _useTokenTransfers = (scope: SearchScope, params: undefined | GetRuntimeEventsParams) => { + if (params && Object.values(params).some(value => value === undefined || value === null)) { + throw new Error('Must set params=undefined while some values are unavailable') + } + const { network, layer } = scope const pagination = useComprehensiveSearchParamsPagination({ paramName: 'page', @@ -47,17 +68,6 @@ export const useTokenTransfers = (scope: SearchScope, address: string, nftInstan // Loading transactions on the consensus layer is not supported yet. // We should use useGetConsensusTransactions() } - - const oasisAddress = useTransformToOasisAddress(address) - const params = nftInstanceId - ? { - nft_id: nftInstanceId, - contract_address: oasisAddress!, - } - : { - rel: oasisAddress!, - } - const query = useGetRuntimeEvents( network, layer, // This is OK since consensus has been handled separately @@ -70,7 +80,7 @@ export const useTokenTransfers = (scope: SearchScope, address: string, nftInstan }, { query: { - enabled: !!oasisAddress, + enabled: !!params, }, }, )