Skip to content

Commit

Permalink
Merge branch '659-implement-back-up-query-notification-for-stale-nft-…
Browse files Browse the repository at this point in the history
…indexer-data' of https://github.com/telosnetwork/telos-wallet into 659-implement-back-up-query-notification-for-stale-nft-indexer-data
  • Loading branch information
Viterbo committed Nov 16, 2023
2 parents 6ae3c44 + 063a310 commit e195f44
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 148 deletions.
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions src/antelope/chains/EVMChainSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { ethers } from 'ethers';
import { toStringNumber } from 'src/antelope/stores/utils/currency-utils';
import { dateIsWithinXMinutes } from 'src/antelope/stores/utils/date-utils';
import { CURRENT_CONTEXT, getAntelope, useContractStore, useNftsStore } from 'src/antelope';
import { WEI_PRECISION } from 'src/antelope/stores/utils';
import { WEI_PRECISION, PRICE_UPDATE_INTERVAL_IN_MIN } from 'src/antelope/stores/utils';


export default abstract class EVMChainSettings implements ChainSettings {
Expand Down Expand Up @@ -331,10 +331,12 @@ export default abstract class EVMChainSettings implements ChainSettings {
const balance = ethers.BigNumber.from(result.balance);
const tokenBalance = new TokenBalance(token, balance);
tokens.push(tokenBalance);
const priceUpdatedWithinTenMins = !!contractData.calldata.marketdata_updated && dateIsWithinXMinutes(+contractData.calldata.marketdata_updated, 10);
const priceIsCurrent =
!!contractData.calldata.marketdata_updated &&
dateIsWithinXMinutes(+contractData.calldata.marketdata_updated, PRICE_UPDATE_INTERVAL_IN_MIN);

// If we have market data we use it, as long as the price was updated within the last 10 minutes
if (typeof contractData.calldata === 'object' && priceUpdatedWithinTenMins) {
if (typeof contractData.calldata === 'object' && priceIsCurrent) {
const price = (+(contractData.calldata.price ?? 0)).toFixed(12);
const marketInfo = { ...contractData.calldata, price } as MarketSourceInfo;
const marketData = new TokenMarketData(marketInfo);
Expand Down Expand Up @@ -388,6 +390,13 @@ export default abstract class EVMChainSettings implements ChainSettings {
};
const response = (await this.indexer.get(url, { params: paramsWithSupply })).data as IndexerAccountNftsResponse;

// If the contract does not have the list of supported interfaces, we provide one
Object.values(response.contracts).forEach((contract) => {
if (contract.supportedInterfaces === null) {
contract.supportedInterfaces = [params.type];
}
});

// the indexer NFT data which will be used to construct NFTs
const shapedIndexerNftData: GenericIndexerNft[] = response.results.map(nftResponse => ({
metadata: JSON.parse(nftResponse.metadata),
Expand Down
30 changes: 26 additions & 4 deletions src/antelope/stores/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* contracts on the blockchain. It also includes support for decoding and encoding data
* using contract ABI, which describes the interface of the contract.
*
* file: src/antelope/stores/contract.ts
*/


Expand All @@ -17,6 +18,7 @@ import {
useFeedbackStore,
useChainStore,
useEVMStore,
getAntelope,
} from 'src/antelope';
import {
AntelopeError,
Expand Down Expand Up @@ -44,9 +46,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)),
});
Expand Down Expand Up @@ -162,14 +171,27 @@ export const useContractStore = defineStore(store_name, {
}

// if we have it in cache, return it
if (typeof this.__contracts[network].cached[addressLower] !== 'undefined') {
if (
// Only if we have the contract
typeof this.__contracts[network].cached[addressLower] !== 'undefined' &&
// and the metadata
typeof this.__contracts[network].metadata[addressLower] !== 'undefined' &&
(
// and either we don't have a interface type
!suspectedToken ||
// the the contract supports the interface type
(this.__contracts[network].metadata[addressLower]?.supportedInterfaces ?? []).includes(suspectedToken)
)
) {
this.trace('getContract', 'returning cached contract', address, [this.__contracts[network].cached[addressLower]]);
return this.__contracts[network].cached[addressLower];
}

// if we have the metadata, we can create the contract and return it
if (typeof this.__contracts[network].metadata[addressLower] !== 'undefined') {
const metadata = this.__contracts[network].metadata[addressLower] as EvmContractFactoryData;
// we ensure the contract hast the proper list of supported interfaces
metadata.supportedInterfaces = metadata.supportedInterfaces || (suspectedToken ? [suspectedToken] : undefined);
this.trace('getContract', 'returning cached metadata', address, [metadata]);
return this.createAndStoreContract(label, addressLower, metadata);
}
Expand Down
9 changes: 8 additions & 1 deletion src/antelope/stores/utils/contracts/EvmContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/antelope/stores/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
const REVERT_FUNCTION_SELECTOR = '0x08c379a0';
const REVERT_PANIC_SELECTOR = '0x4e487b71';

export const PRICE_UPDATE_INTERVAL_IN_MIN = 30;
export const WEI_PRECISION = 18;

/**
Expand Down
36 changes: 36 additions & 0 deletions src/antelope/stores/utils/nft-utils.ts
Original file line number Diff line number Diff line change
@@ -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
*
Expand Down Expand Up @@ -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<NftSourceType>} - the media type
*/
export async function determineIpfsMediaType(url: string): Promise<NftSourceType> {
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');
}
}
2 changes: 1 addition & 1 deletion src/antelope/types/EvmContractData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface EvmContractConstructorData {
}

export interface EvmContractManagerI {
getSigner: () => Promise<ethers.Signer>;
getSigner: () => Promise<ethers.Signer | null>;
getWeb3Provider: () => Promise<ethers.providers.Web3Provider>;
getFunctionIface: (hash:string) => Promise<ethers.utils.Interface | null>;
getEventIface: (hash:string) => Promise<ethers.utils.Interface | null>;
Expand Down
8 changes: 5 additions & 3 deletions src/antelope/types/NFTClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,7 +132,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)
Expand All @@ -143,6 +143,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,
Expand All @@ -161,7 +163,7 @@ export async function constructNft(
};

if (isErc721) {
const contractInstance = await (await contractStore.getContract(CURRENT_CONTEXT, contract.address))?.getContractInstance();
const contractInstance = await (await contractStore.getContract(CURRENT_CONTEXT, contract.address, 'erc721'))?.getContractInstance();

if (!contractInstance) {
throw new AntelopeError('antelope.utils.error_contract_instance');
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en-us/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default {
link_to_receive_aria: 'Link to Receive page',
link_to_buy_aria: 'External link to buy tokens',
balance_row_actions_aria: 'Balance row actions',
no_fiat_value: 'No reliable fiat value found',
no_fiat_value: 'current fiat price unavailable',
receiving_account: 'Receiving Account',
account_required: 'Account is required',
token: 'Token',
Expand Down
Loading

0 comments on commit e195f44

Please sign in to comment.