diff --git a/jest.config.js b/jest.config.js index 7c25bf132..973ee945b 100755 --- a/jest.config.js +++ b/jest.config.js @@ -28,10 +28,10 @@ module.exports = { //TODO increase thresholds as coverage increases as testing will be enforced coverageThreshold: { global: { - statements: 4.06, + statements: 4.02, branches: 4.84, functions: 1.34, - lines: 4.21, + lines: 4.16, }, './src/components/': { statements: 0, diff --git a/src/antelope/stores/contract.ts b/src/antelope/stores/contract.ts index db3926899..e2736fc9f 100644 --- a/src/antelope/stores/contract.ts +++ b/src/antelope/stores/contract.ts @@ -17,6 +17,7 @@ import { useFeedbackStore, useChainStore, useEVMStore, + getAntelope, } from 'src/antelope'; import { AntelopeError, @@ -44,9 +45,16 @@ import { toRaw } from 'vue'; const LOCAL_SORAGE_CONTRACTS_KEY = 'antelope.contracts'; -const createManager = (authenticator: EVMAuthenticator):EvmContractManagerI => ({ - getSigner: async () => toRaw((await authenticator.web3Provider()).getSigner(useAccountStore().getAccount(authenticator.label).account)), - getWeb3Provider: () => authenticator.web3Provider(), +const createManager = (authenticator?: EVMAuthenticator):EvmContractManagerI => ({ + getSigner: async () => { + if (!authenticator) { + return null; + } + const provider = await authenticator.web3Provider(); + const account = useAccountStore().getAccount(authenticator.label).account; + return provider.getSigner(account); + }, + getWeb3Provider: () => getAntelope().wallets.getWeb3Provider(), getFunctionIface: (hash:string) => toRaw(useEVMStore().getFunctionIface(hash)), getEventIface: (hash:string) => toRaw(useEVMStore().getEventIface(hash)), }); diff --git a/src/antelope/stores/utils/contracts/EvmContract.ts b/src/antelope/stores/utils/contracts/EvmContract.ts index c321cd007..1a1571d79 100644 --- a/src/antelope/stores/utils/contracts/EvmContract.ts +++ b/src/antelope/stores/utils/contracts/EvmContract.ts @@ -138,8 +138,15 @@ export default class EvmContract { throw new AntelopeError('antelope.utils.error_contract_instance'); } const signer = await this._manager?.getSigner(); + let provider; - return new ethers.Contract(this.address, this.abi, signer); + if (!signer) { + provider = await this._manager?.getWeb3Provider(); + } + + const contract = new ethers.Contract(this.address, this.abi, signer ?? provider ?? undefined); + + return contract; } async parseTransaction(data:string) { diff --git a/src/antelope/stores/utils/nft-utils.ts b/src/antelope/stores/utils/nft-utils.ts index 85ee0c52b..6925ecb59 100644 --- a/src/antelope/stores/utils/nft-utils.ts +++ b/src/antelope/stores/utils/nft-utils.ts @@ -1,6 +1,8 @@ import { IndexerNftMetadata, NFTSourceTypes, NftSourceType } from 'src/antelope/types'; import { urlIsAudio, urlIsPicture, urlIsVideo } from 'src/antelope/stores/utils/media-utils'; +export const IPFS_GATEWAY = 'https://cloudflare-ipfs.com/ipfs/'; + /** * Given an imageCache URL, tokenUri, and metadata, extract the image URL, mediaType, and mediaSource * @@ -107,5 +109,39 @@ export async function extractNftMetadata( } } + if (metadata?.image?.includes(IPFS_GATEWAY)) { + mediaType = await determineIpfsMediaType(metadata?.image); + + if (mediaType === NFTSourceTypes.IMAGE) { + image = metadata?.image; + } + mediaSource = metadata?.image; + } + return { image, mediaType, mediaSource }; } + + +/** + * Given an IPFS media URL, determine the media type + * @param url - the IPFS media URL + * @returns {Promise} - the media type + */ +export async function determineIpfsMediaType(url: string): Promise { + try { + const response = await fetch(url); + const contentType = response.headers.get('Content-Type') ?? ''; + + if (contentType.startsWith('image/')) { + return NFTSourceTypes.IMAGE; + } else if (contentType.startsWith('video/')) { + return NFTSourceTypes.VIDEO; + } else if (contentType.startsWith('audio/')) { + return NFTSourceTypes.AUDIO; + } else { + return NFTSourceTypes.UNKNOWN; + } + } catch (error) { + throw new Error('Error determining IPFS media type'); + } +} diff --git a/src/antelope/types/EvmContractData.ts b/src/antelope/types/EvmContractData.ts index 8d8b9e930..05ac925ad 100644 --- a/src/antelope/types/EvmContractData.ts +++ b/src/antelope/types/EvmContractData.ts @@ -17,7 +17,7 @@ export interface EvmContractConstructorData { } export interface EvmContractManagerI { - getSigner: () => Promise; + getSigner: () => Promise; getWeb3Provider: () => Promise; getFunctionIface: (hash:string) => Promise; getEventIface: (hash:string) => Promise; diff --git a/src/antelope/types/NFTClass.ts b/src/antelope/types/NFTClass.ts index 2f5a6f2ed..6681a3cbd 100644 --- a/src/antelope/types/NFTClass.ts +++ b/src/antelope/types/NFTClass.ts @@ -8,7 +8,7 @@ import { IndexerNftMetadata, IndexerTokenHoldersResponse, } from 'src/antelope/types/IndexerTypes'; -import { extractNftMetadata } from 'src/antelope/stores/utils/nft-utils'; +import { IPFS_GATEWAY, extractNftMetadata } from 'src/antelope/stores/utils/nft-utils'; import { useContractStore } from 'src/antelope/stores/contract'; import { useNftsStore } from 'src/antelope/stores/nfts'; import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; @@ -118,7 +118,7 @@ export async function constructNft( console.error('Error parsing metadata', `"${indexerData.metadata}"`, e); } if (!indexerData.metadata || typeof indexerData.metadata !== 'object') { - // we create a new metadata object with the actual string atributes of the item + // we create a new metadata object with the actual string attributes of the item const list = indexerData as unknown as { [key: string]: unknown }; indexerData.metadata = Object.keys(indexerData) @@ -129,6 +129,8 @@ export async function constructNft( }, {} as { [key: string]: string }); } + indexerData.metadata.image = ((indexerData.metadata.image as string) ?? '').replace('ipfs://', IPFS_GATEWAY); + const { image, mediaType, mediaSource } = await extractNftMetadata(indexerData.imageCache ?? '', indexerData.tokenUri ?? '', indexerData.metadata ?? {}); const commonData: NftPrecursorData = { name: (indexerData.metadata?.name ?? '') as string, diff --git a/src/antelope/wallets/index.ts b/src/antelope/wallets/index.ts index 845b0d408..53f7baaea 100644 --- a/src/antelope/wallets/index.ts +++ b/src/antelope/wallets/index.ts @@ -2,6 +2,12 @@ import { isTracingAll } from 'src/antelope/stores/feedback'; import { useFeedbackStore } from 'src/antelope/stores/feedback'; import { createTraceFunction } from 'src/antelope/stores/feedback'; import { EVMAuthenticator } from 'src/antelope/wallets/authenticators/EVMAuthenticator'; +import { useAccountStore } from 'src/antelope/stores/account'; +import { CURRENT_CONTEXT, useChainStore } from 'src/antelope'; +import { RpcEndpoint } from 'universal-authenticator-library'; +import { ethers } from 'ethers'; +import EVMChainSettings from 'src/antelope/chains/EVMChainSettings'; +import { AntelopeError } from 'src/antelope/types'; const name = 'AntelopeWallets'; @@ -27,6 +33,46 @@ export class AntelopeWallets { return this.authenticators.get(name); } + getChainSettings(label: string) { + return (useChainStore().getChain(label).settings as EVMChainSettings); + } + + async getWeb3Provider(): Promise { + this.trace('getWeb3Provider'); + const account = useAccountStore().getAccount(CURRENT_CONTEXT); + try { + // we try first the best solution which is taking the provider from the current authenticator + const authenticator = account.authenticator as EVMAuthenticator; + const provider = authenticator.web3Provider(); + return provider; + } catch(e1) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e1); + } + + // we try to build a web3 provider from a local injected provider it it exists + try { + if (window.ethereum) { + const web3Provider = new ethers.providers.Web3Provider(window.ethereum); + await web3Provider.ready; + return web3Provider; + } + } catch(e2) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e2); + } + + try { + const p:RpcEndpoint = this.getChainSettings(CURRENT_CONTEXT).getRPCEndpoint(); + const url = `${p.protocol}://${p.host}:${p.port}${p.path ?? ''}`; + const jsonRpcProvider = new ethers.providers.JsonRpcProvider(url); + await jsonRpcProvider.ready; + const web3Provider = jsonRpcProvider as ethers.providers.Web3Provider; + return web3Provider; + } catch (e3) { + this.trace('getWeb3Provider authenticator.web3Provider() Failed!', e3); + throw new AntelopeError('antelope.evn.error_no_provider'); + } + } + } export * from 'src/antelope/wallets/authenticators/EVMAuthenticator'; diff --git a/src/pages/evm/nfts/NftDetailsPage.vue b/src/pages/evm/nfts/NftDetailsPage.vue index 3ae629040..c7de1dd31 100644 --- a/src/pages/evm/nfts/NftDetailsPage.vue +++ b/src/pages/evm/nfts/NftDetailsPage.vue @@ -226,16 +226,35 @@ function removeTab(tab: Tab){ :class="{ 'c-nft-details__header-container': true, 'c-nft-details__header-container--grid': nft !== null || loading, + 'row': true, }" > @@ -420,35 +452,19 @@ function removeTab(tab: Tab){ $this: &; &__header-container { + flex-direction: column; + &--grid { - display: grid; - grid-template-areas: - 'a' - 'b' - 'c' - 'd' - 'e'; - gap: 8px; width: 100%; + flex-direction: row; + @include sm-and-up { - grid-template-areas: - 'a a' - 'b b' - 'c d' - 'e e'; max-width: 432px; } @include md-and-up { - grid-template-areas: - 'a a a b b' - 'a a a c d' - 'a a a e e' - 'a a a . .'; max-width: 1000px; - width: max-content; - grid-template-columns: repeat(5, 1fr); } } } @@ -458,48 +474,19 @@ function removeTab(tab: Tab){ max-width: 1000px; } - &__viewer { - grid-area: a; - } - &__header-card { - &:nth-of-type(2) { - grid-area: b; - } - - &:nth-of-type(3) { - grid-area: c; - } - - &:nth-of-type(4) { - grid-area: d; - } - - &:nth-of-type(5) { - grid-area: e; - } - - &--placeholder { - margin: auto; - height: 80px; - width: 100%; - - &:first-of-type { - grid-area: a; - width: 270px; - height: 270px; - - @include md-and-up { - width: 432px; - height: 432px; - } - } - } + min-height: 88px; } &__header-skeleton { - height: 100%; + height: 88px; width: 100%; + + &--large { + margin: auto; + height: 270px; + max-width: 270px; + } } &__attributes-container { diff --git a/src/pages/evm/nfts/NftInventoryPage.vue b/src/pages/evm/nfts/NftInventoryPage.vue index 4be01b9e5..7e5127d32 100644 --- a/src/pages/evm/nfts/NftInventoryPage.vue +++ b/src/pages/evm/nfts/NftInventoryPage.vue @@ -1,5 +1,6 @@