diff --git a/cspell.json b/cspell.json index 5b6df692d4fc..9dc41c29c4a6 100644 --- a/cspell.json +++ b/cspell.json @@ -147,6 +147,7 @@ "metas", "metaverse", "millify", + "mintable", "misoperation", "monofont", "monospace", @@ -174,6 +175,7 @@ "pid", "pids", "Plurinational", + "plusplus", "Polkadot", "Pooltogether", "popper", @@ -257,8 +259,7 @@ "xlarge", "xlink", "zerion", - "Zubin", - "mintable" + "Zubin" ], "ignoreWords": [ "AICANFT", diff --git a/packages/mask/src/plugins/Avatar/Services/bind.ts b/packages/mask/src/plugins/Avatar/Services/bind.ts new file mode 100644 index 000000000000..4c6a0e96e9f7 --- /dev/null +++ b/packages/mask/src/plugins/Avatar/Services/bind.ts @@ -0,0 +1,92 @@ +import { ChainId, isSameAddress } from '@masknet/web3-shared-evm' +import { NetworkPluginID } from '@masknet/plugin-infra' +import addSeconds from 'date-fns/addSeconds' +import { KeyValue } from '@masknet/web3-providers' +import { NFT_AVATAR_DB_NAME, NFT_AVATAR_DB_NAME_STORAGE } from '../constants' +import { gun2 } from '../../../network/gun/version.2' +import { delay } from '@masknet/shared-base' + +const NFTAvatarGUN = gun2.get(NFT_AVATAR_DB_NAME) +const READ_GUN_RETRIES = 10 + +const NFTAvatarDB = KeyValue.createJSON_Storage(NFT_AVATAR_DB_NAME) +const NFTAvatarDBStorage = KeyValue.createJSON_Storage(NFT_AVATAR_DB_NAME_STORAGE) + +const cache = new Map, number]>() + +async function getUserAddressFromGUN(userId: string): Promise { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < READ_GUN_RETRIES; i++) { + const result = await NFTAvatarGUN + // @ts-expect-error + .get(userId).then!() + if (result) return result + await delay(500) + } + return +} + +function getKey(networkPluginId = NetworkPluginID.PLUGIN_EVM, chainId: number = ChainId.Mainnet) { + return `${networkPluginId}-${chainId}` +} + +function getCacheKey(userId: string, networkPluginId = NetworkPluginID.PLUGIN_EVM, chainId: number = ChainId.Mainnet) { + return `${userId}-${networkPluginId}-${chainId}` +} + +async function _getUserAddress(userId: string, networkPluginId?: NetworkPluginID, chainId?: number) { + try { + const result = await NFTAvatarDB.get<{ networkPluginId: string; chainId: number; address: string }>(userId) + if (!result || !result?.address) { + const result = await NFTAvatarDBStorage.get>(userId) + const address = result?.[getKey(networkPluginId, chainId)] + if (address) return address + return getUserAddressFromGUN(userId) + } + return result.address + } catch { + return getUserAddressFromGUN(userId) + } +} + +export async function getUserAddress(userId: string, networkPluginId?: NetworkPluginID, chainId?: number) { + let c = cache.get(getCacheKey(userId, networkPluginId, chainId)) + if (!c || Date.now() > c[1]) { + try { + cache.set(getCacheKey(userId, networkPluginId, chainId), [ + _getUserAddress(userId, networkPluginId, chainId), + addSeconds(new Date(), 60).getTime(), + ]) + } catch (err) { + console.log(err) + } + } + c = cache.get(getCacheKey(userId, networkPluginId, chainId)) + + return c?.[0] +} + +export async function setUserAddress( + userId: string, + address: string, + networkPluginId?: NetworkPluginID, + chainId?: number, +) { + try { + await NFTAvatarDBStorage.set>(userId, { + [getKey(networkPluginId, chainId)]: address, + }) + + await NFTAvatarDB.set<{ networkPluginId: string; chainId: number; address: string }>(userId, { + networkPluginId: networkPluginId ?? NetworkPluginID.PLUGIN_EVM, + chainId: chainId ?? ChainId.Mainnet, + address, + }) + } catch { + // do nothing + } finally { + const _address = await getUserAddress(userId, networkPluginId, chainId) + if (!isSameAddress(_address, address)) + throw new Error('Network issues, please make sure you are connected to the appropriate internet.') + } +} diff --git a/packages/mask/src/plugins/Avatar/Services/gun.ts b/packages/mask/src/plugins/Avatar/Services/gun.ts deleted file mode 100644 index 98bad23398cc..000000000000 --- a/packages/mask/src/plugins/Avatar/Services/gun.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { delay } from '@masknet/shared-base' -import { isSameAddress } from '@masknet/web3-shared-evm' -import { gun2 } from '../../../network/gun/version.2' -import { NFT_AVATAR_GUN_SERVER } from '../constants' - -const NFTAvatarGUN = gun2.get(NFT_AVATAR_GUN_SERVER) - -// After reinstalling the system, it cannot be retrieved for the first time, so it needs to be taken twice -export async function getUserAddress(userId: string) { - let result = await NFTAvatarGUN - // @ts-expect-error - .get(userId).then!() - - if (!result) { - await delay(500) - result = await NFTAvatarGUN - // @ts-expect-error - .get(userId).then!() - } - - return result -} - -export async function setUserAddress(userId: string, address: string) { - try { - // delete userId - await NFTAvatarGUN - // @ts-expect-error - .get(userId) - // @ts-expect-error - .put(null).then!() - - // save userId - await NFTAvatarGUN - // @ts-expect-error - .get(userId) - // @ts-expect-error - .put(address).then!() - } catch { - // do nothing - } finally { - const _address = await getUserAddress(userId) - if (!isSameAddress(_address, address)) - throw new Error('Something went wrong, and please check your connection.') - } -} - -export async function getUserAddresses() { - const NFTAvatarKeys = Object.keys(await NFTAvatarGUN).filter((x) => x !== '_') - const resultPromise = NFTAvatarKeys.map((key) => getUserAddress(key)) - const result = (await Promise.all(resultPromise)).filter((x) => x) - - return result -} diff --git a/packages/mask/src/plugins/Avatar/Services/index.ts b/packages/mask/src/plugins/Avatar/Services/index.ts index 3ed77abe52f8..0abf57fdebf0 100644 --- a/packages/mask/src/plugins/Avatar/Services/index.ts +++ b/packages/mask/src/plugins/Avatar/Services/index.ts @@ -1,39 +1,38 @@ -import { personalSign } from '../../../extension/background-script/EthereumService' +import type { NetworkPluginID } from '@masknet/plugin-infra' import type { AvatarMetaDB } from '../types' import { getNFTAvatarFromJSON } from './db' -import { getUserAddress, setUserAddress } from './gun' +import { getUserAddress, setUserAddress } from './bind' import { getNFTAvatarFromRSS, saveNFTAvatarToRSS } from './rss3' -export async function getNFTAvatar(userId: string) { - let result - const address = await getUserAddress(userId) - if (!address) { - result = await getNFTAvatarFromJSON(userId) - return result +export async function getNFTAvatar(userId: string, networkPluginId?: NetworkPluginID, chainId?: number) { + const address = await getUserAddress(userId, networkPluginId, chainId) + if (address) { + return getNFTAvatarFromRSS(userId, address) } - - result = await getNFTAvatarFromRSS(userId, address) - if (!result) { - result = await getNFTAvatarFromJSON(userId) - } - - return result + return getNFTAvatarFromJSON(userId) } -export async function saveNFTAvatar(address: string, nft: AvatarMetaDB) { - const signature = await personalSign(nft.userId, address) - setUserAddress(nft.userId, address) - const avatar = await saveNFTAvatarToRSS(address, nft, signature) - return avatar +export async function saveNFTAvatar( + address: string, + nft: AvatarMetaDB, + networkPluginId?: NetworkPluginID, + chainId?: number, +) { + try { + const avatar = await saveNFTAvatarToRSS(address, nft, '') + await setUserAddress(nft.userId, address, networkPluginId, chainId) + return avatar + } catch (error) { + throw error + } } -export async function getAddress(userId: string) { - const address = await getUserAddress(userId) +export async function getAddress(userId: string, networkPluginId?: NetworkPluginID, chainId?: number) { + const address = await getUserAddress(userId, networkPluginId, chainId) return (address ?? '') as string } export { getNFTContractVerifiedFromJSON } from './verified' -export { getUserAddresses } from './gun' export async function getImage(image: string): Promise { const response = await globalThis.fetch(image) diff --git a/packages/mask/src/plugins/Avatar/Services/rss3.ts b/packages/mask/src/plugins/Avatar/Services/rss3.ts index 552200519e40..5f3a5aa2388d 100644 --- a/packages/mask/src/plugins/Avatar/Services/rss3.ts +++ b/packages/mask/src/plugins/Avatar/Services/rss3.ts @@ -1,5 +1,3 @@ -import Web3 from 'web3' -import { isSameAddress } from '@masknet/web3-shared-evm' import { personalSign } from '../../../extension/background-script/EthereumService' import type { AvatarMetaDB } from '../types' import addSeconds from 'date-fns/addSeconds' @@ -10,61 +8,29 @@ interface NFTRSSNode { nft: AvatarMetaDB } -const cache = new Map< - string, - [Promise<{ type: string; nfts: Record | NFTRSSNode } | undefined>, number] ->() +const cache = new Map, number]>() export async function getNFTAvatarFromRSS(userId: string, address: string) { let v = cache.get(address) if (!v || Date.now() > v[1]) { - cache.set(address, [_getNFTAvatarFromRSS(address), addSeconds(Date.now(), 60).getTime()]) + cache.set(address, [_getNFTAvatarFromRSS(userId, address), addSeconds(Date.now(), 60).getTime()]) } v = cache.get(address) const result = await v?.[0] - if (!result) return - const { type, nfts } = result - - const web3 = new Web3() - let nft: NFTRSSNode - if (type === 'NFTS') { - const data = nfts as Record - nft = data[userId] - } else { - nft = nfts as NFTRSSNode - } - - try { - const sig_address = web3.eth.accounts.recover(nft.nft.userId, nft.signature) - if (!isSameAddress(sig_address, address)) return - return nft.nft - } catch { - throw new Error('Failed to recover signature.') - } + return result?.nft } -async function _getNFTAvatarFromRSS( - address: string, -): Promise<{ type: string; nfts: Record | NFTRSSNode } | undefined> { +async function _getNFTAvatarFromRSS(userId: string, address: string): Promise { const rss = RSS3.createRSS3(address, async (message: string) => { return personalSign(message, address) }) const nfts = await RSS3.getFileData>(rss, address, '_nfts') if (nfts) { - return { - type: 'NFTS', - nfts, - } - } else { - const nft = await RSS3.getFileData(rss, address, '_nft') - if (!nft) return - return { - type: 'NFT', - nfts: nft, - } + return nfts[userId] } + return RSS3.getFileData(rss, address, '_nft') } export async function saveNFTAvatarToRSS(address: string, nft: AvatarMetaDB, signature: string) { diff --git a/packages/mask/src/plugins/Avatar/constants.ts b/packages/mask/src/plugins/Avatar/constants.ts index a367a3274058..ec2a44085c2a 100644 --- a/packages/mask/src/plugins/Avatar/constants.ts +++ b/packages/mask/src/plugins/Avatar/constants.ts @@ -1,7 +1,8 @@ import { PluginId } from '@masknet/plugin-infra' export const NFT_AVATAR_JSON_SERVER = 'https://configuration.r2d2.to/com.maskbook.avatar.json' -export const NFT_AVATAR_GUN_SERVER = 'com.maskbook.user' +export const NFT_AVATAR_DB_NAME = 'com.maskbook.user' +export const NFT_AVATAR_DB_NAME_STORAGE = 'com.maskbook.user.storage' export const NFT_CONTRACT_JSON_VERIFIED_SERVER = 'https://configuration.r2d2.to/com.maskbook.verified_nft.json' export const PLUGIN_ID = PluginId.Avatar diff --git a/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts b/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts index 472de6042782..6e418345bc87 100644 --- a/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts +++ b/packages/mask/src/plugins/Avatar/hooks/useNFTAvatar.ts @@ -5,8 +5,7 @@ import type { AvatarMetaDB } from '../types' export function useNFTAvatar(userId: string): AsyncState { return useAsync(async () => { - if (!userId) return - if (userId === '$unknown') return + if (!userId || userId === '$unknown') return return PluginNFTAvatarRPC.getNFTAvatar(userId) }, [userId]) } diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx index 34850160d6d8..af0b79383cca 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/NFT/NFTAvatarInTwitter.tsx @@ -89,7 +89,18 @@ function NFTAvatarInTwitter() { const avatar = await PluginNFTAvatarRPC.saveNFTAvatar(wallet.address, { ...NFTEvent, avatarId: getAvatarId(identity.avatar ?? ''), - } as AvatarMetaDB) + } as AvatarMetaDB).catch((error) => { + setNFTEvent(undefined) + setAvatar(undefined) + window.alert(error.message) + return + }) + if (!avatar) { + setNFTEvent(undefined) + setAvatar(undefined) + window.alert('Sorry, failed to save NFT Avatar. Please set again.') + return + } setAvatar(avatar) MaskMessages.events.NFTAvatarTimelineUpdated.sendToAll( diff --git a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx index e3572ae5c7a8..0a2f2be0ed99 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx +++ b/packages/mask/src/social-network-adaptor/twitter.com/injection/ProfileTab.tsx @@ -135,11 +135,11 @@ export function ProfileTabAtTwitter() { } export function injectProfileTabAtTwitter(signal: AbortSignal) { - const watcher = new MutationObserverWatcher(searchProfileTabListLastChildSelector()) let tabInjected = false const contentWatcher = new MutationObserverWatcher(searchProfileTabPageSelector()).useForeach((node, key, meta) => { const elePage = searchProfileTabPageSelector().evaluate() if (elePage && !tabInjected) { + const watcher = new MutationObserverWatcher(searchProfileTabListLastChildSelector()) startWatch(watcher, signal) createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render() tabInjected = true diff --git a/packages/web3-constants/evm/debank.json b/packages/web3-constants/evm/debank.json index c511f4a3a22c..18aea1893f65 100644 --- a/packages/web3-constants/evm/debank.json +++ b/packages/web3-constants/evm/debank.json @@ -15,10 +15,6 @@ "Celo": "celo", "Fantom": "ftm", "Aurora": "aurora", - "Aurora_Testnet": "", - "Avalanche": "avax", - "Boba": "boba", - "Metis": "metis", - "Optimistic": "op" + "Aurora_Testnet": "" } } diff --git a/packages/web3-providers/src/opensea/index.ts b/packages/web3-providers/src/opensea/index.ts index 9c456d5f6c24..f9e76a00620b 100644 --- a/packages/web3-providers/src/opensea/index.ts +++ b/packages/web3-providers/src/opensea/index.ts @@ -34,6 +34,7 @@ async function fetchFromOpenSea(url: string, chainId: ChainId, apiKey?: strin headers: { 'x-api-key': apiKey ?? OPENSEA_API_KEY, Accept: 'application/json' }, ...(currentEnv === NonFungibleTokenAPI.APIEnv.browser && { mode: 'cors' }), }) + if (response.status === 404) return return response.json() as Promise } catch { return diff --git a/packages/web3-providers/src/types.ts b/packages/web3-providers/src/types.ts index 71740686187f..143afadedd3e 100644 --- a/packages/web3-providers/src/types.ts +++ b/packages/web3-providers/src/types.ts @@ -291,13 +291,13 @@ export namespace NonFungibleTokenAPI { export namespace StorageAPI { export interface Storage { set(key: string, value: T): Promise - get(key: string): Promise - delete?: (key: string) => Promise + get(key: string): Promise + delete?(key: string): Promise } export interface Provider { - createJSON_Storage?: (key: string) => Storage - createBinaryStorage?: (key: string) => Storage + createJSON_Storage?(key: string): Storage + createBinaryStorage?(key: string): Storage } }