From 9554b14ad28bb78af2580670234f9cb11c8ba9b6 Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Fri, 1 Oct 2021 13:22:24 +0800 Subject: [PATCH 01/13] chore: bump version to 1.37.7 --- package.json | 2 +- packages/maskbook/src/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 361ae73805d8..6f67974131b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "maskbook", - "version": "1.37.6", + "version": "1.37.7", "private": true, "license": "AGPL-3.0-or-later", "scripts": { diff --git a/packages/maskbook/src/manifest.json b/packages/maskbook/src/manifest.json index d6e514d112e4..420261627fae 100644 --- a/packages/maskbook/src/manifest.json +++ b/packages/maskbook/src/manifest.json @@ -1,6 +1,6 @@ { "name": "Mask Network", - "version": "1.37.6", + "version": "1.37.7", "manifest_version": 2, "permissions": ["storage", "downloads", "webNavigation", "activeTab"], "optional_permissions": ["", "notifications", "clipboardRead"], From aa04a1cfd3d794e9d6108eeebe40569c8a862959 Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Fri, 8 Oct 2021 19:03:19 +0800 Subject: [PATCH 02/13] fix: the tab style should follow theme color (#4554) --- .../plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx | 20 +++++++------------ .../SNSAdaptor/RedPacketCreateNew.tsx | 9 ++++----- .../RedPacket/SNSAdaptor/RedPacketDialog.tsx | 5 +---- .../RedPacket/SNSAdaptor/RedPacketPast.tsx | 9 ++++----- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/packages/maskbook/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx b/packages/maskbook/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx index 4ea283eed5a4..a81dfac98112 100644 --- a/packages/maskbook/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx +++ b/packages/maskbook/src/plugins/ITO/SNSAdaptor/ClaimAllDialog.tsx @@ -1,7 +1,7 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useLayoutEffect, useRef } from 'react' import { flatten, uniq } from 'lodash-es' import formatDateTime from 'date-fns/format' -import { getMaskColor, useSnackbar, VariantType, SnackbarProvider } from '@masknet/theme' +import { useSnackbar, VariantType, SnackbarProvider } from '@masknet/theme' import { FormattedBalance, useRemoteControlledDialog } from '@masknet/shared' import { DialogContent, CircularProgress, Typography, List, ListItem, useTheme } from '@material-ui/core' import { makeStyles } from '@masknet/theme' @@ -26,7 +26,6 @@ import { useClaimCallback } from './hooks/useClaimCallback' import ActionButton from '../../../extension/options-page/DashboardComponents/ActionButton' import { EthereumWalletConnectedBoundary } from '../../../web3/UI/EthereumWalletConnectedBoundary' import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' -import { useLayoutEffect, useRef } from 'react' interface StyleProps { shortITOwrapper: boolean @@ -132,19 +131,18 @@ const useStyles = makeStyles()((theme, props) => ({ height: 36, minHeight: 36, fontWeight: 300, - color: theme.palette.mode === 'light' ? '#15181B' : '#D9D9D9', }, tabs: { - backgroundColor: getMaskColor(theme).twitterBackground, width: 536, height: 36, minHeight: 36, margin: '0 auto', + borderRadius: 4, + backgroundColor: theme.palette.background.default, '& .Mui-selected': { - backgroundColor: '#1C68F3', - color: '#fff', + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, }, - borderRadius: 4, }, indicator: { display: 'none', @@ -420,12 +418,8 @@ export function ClaimAllDialog(props: ClaimAllDialogProps) { chainId={chainId} noSwitchNetworkTip={true} switchButtonStyle={{ - backgroundColor: '#1C68F3', - '&:hover': { - backgroundColor: '#1854c4', - }, minHeight: 'auto', - width: '540px', + width: 540, fontSize: 18, fontWeight: 400, }}> diff --git a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketCreateNew.tsx b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketCreateNew.tsx index 49d5821c1bf7..9183a063ff41 100644 --- a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketCreateNew.tsx +++ b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketCreateNew.tsx @@ -17,19 +17,18 @@ const useStyles = makeStyles()((theme, { snsId }) => ({ height: 36, minHeight: 36, fontWeight: 300, - color: theme.palette.mode === 'light' ? '#15181B' : '#D9D9D9', }, tabs: { - backgroundColor: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', width: 544, height: 36, minHeight: 36, margin: `0 ${snsId === MINDS_ID ? '12px' : 'auto'}`, + borderRadius: 4, + backgroundColor: theme.palette.background.default, '& .Mui-selected': { - backgroundColor: '#1C68F3', - color: '#fff !important', + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, }, - borderRadius: 4, }, indicator: { display: 'none', diff --git a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx index c0c5256be9aa..aa814bef3fc2 100644 --- a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx +++ b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx @@ -39,9 +39,6 @@ const useStyles = makeStyles()((theme) => ({ right: 0, position: 'absolute', }, - focusTab: { - backgroundColor: theme.palette.mode === 'light' ? 'rgba(247, 249, 250, 1)' : 'rgba(255, 255, 255, 0.08)', - }, dialogContent: { padding: 0, }, @@ -255,7 +252,7 @@ export default function RedPacketDialog(props: RedPacketDialogProps) { ], state, classes: { - focusTab: classes.focusTab, + // focusTab: classes.focusTab, tabPaper: classes.tabPaper, }, } diff --git a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketPast.tsx b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketPast.tsx index 9e394aafc2c4..c484db6ddc91 100644 --- a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketPast.tsx +++ b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketPast.tsx @@ -21,19 +21,18 @@ const useStyles = makeStyles()((theme) => ({ height: 36, minHeight: 36, fontWeight: 300, - color: theme.palette.mode === 'light' ? '#15181B' : '#D9D9D9', }, tabs: { - backgroundColor: theme.palette.mode === 'light' ? '#F7F9FA' : '#17191D', width: 544, height: 36, minHeight: 36, margin: '0 auto', + borderRadius: 4, + backgroundColor: theme.palette.background.default, '& .Mui-selected': { - backgroundColor: '#1C68F3', - color: '#fff !important', + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main, }, - borderRadius: 4, }, indicator: { display: 'none', From 20143c42475364077b6b1dc58e2e20e6003da53f Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Sat, 9 Oct 2021 15:27:34 +0800 Subject: [PATCH 03/13] fix: track the visiting identity (#4547) --- .../twitter.com/collecting/identity.ts | 34 +++++++++++-------- .../twitter.com/utils/selector.ts | 4 +-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/maskbook/src/social-network-adaptor/twitter.com/collecting/identity.ts b/packages/maskbook/src/social-network-adaptor/twitter.com/collecting/identity.ts index 1d8f88b6a45a..e9b5785a179d 100644 --- a/packages/maskbook/src/social-network-adaptor/twitter.com/collecting/identity.ts +++ b/packages/maskbook/src/social-network-adaptor/twitter.com/collecting/identity.ts @@ -1,6 +1,6 @@ import { isNil } from 'lodash-es' -import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' -import { selfInfoSelectors, searchProfileTabListSelector } from '../utils/selector' +import { LiveSelector, MutationObserverWatcher } from '@dimensiondev/holoflows-kit' +import { selfInfoSelectors, searchAvatarSelector, searchAvatarMetaSelector } from '../utils/selector' import { ProfileIdentifier } from '../../../database/type' import { creator, SocialNetworkUI as Next } from '../../../social-network' import { twitterBase } from '../base' @@ -38,13 +38,15 @@ function resolveCurrentVisitingIdentityInner( ref: Next.CollectingCapabilities.IdentityResolveProvider['recognized'], cancel: AbortSignal, ) { - const headerPhotoSelector = searchProfileTabListSelector() + const avatarSelector = searchAvatarSelector() + const avatarMetaSelector = searchAvatarMetaSelector() const assign = async () => { await delay(500) const bio = getBioDescription() const nickname = getNickname() const handle = getTwitterId() const avatar = getAvatar() + ref.value = { identifier: new ProfileIdentifier(twitterBase.networkIdentifier, handle), nickname, @@ -52,20 +54,24 @@ function resolveCurrentVisitingIdentityInner( bio, } } + const createWatcher = (selector: LiveSelector) => { + const watcher = new MutationObserverWatcher(selector) + .addListener('onAdd', () => assign()) + .addListener('onChange', () => assign()) + .startWatch({ + childList: true, + subtree: true, + }) - const watcher = new MutationObserverWatcher(headerPhotoSelector) - .addListener('onAdd', () => assign()) - .addListener('onChange', () => assign()) - .startWatch({ - childList: true, - subtree: true, + window.addEventListener('locationchange', assign) + cancel.addEventListener('abort', () => { + window.removeEventListener('locationchange', assign) + watcher.stopWatch() }) + } - window.addEventListener('locationchange', assign) - cancel.addEventListener('abort', () => { - window.removeEventListener('locationchange', assign) - watcher.stopWatch() - }) + createWatcher(avatarSelector) + createWatcher(avatarMetaSelector) } export const IdentityProviderTwitter: Next.CollectingCapabilities.IdentityResolveProvider = { diff --git a/packages/maskbook/src/social-network-adaptor/twitter.com/utils/selector.ts b/packages/maskbook/src/social-network-adaptor/twitter.com/utils/selector.ts index dbe51a216e23..c7c3d464a61f 100644 --- a/packages/maskbook/src/social-network-adaptor/twitter.com/utils/selector.ts +++ b/packages/maskbook/src/social-network-adaptor/twitter.com/utils/selector.ts @@ -44,8 +44,8 @@ export const searchNickNameSelector: () => LiveSelector = () => querySelector('[data-testid="UserProfileHeader_Items"]') export const searchAvatarSelector: () => LiveSelector = () => querySelector('[data-testid="primaryColumn"] a[role="link"][href$="/photo"] img') -export const searchHeaderPhotoSelector: () => LiveSelector = () => - querySelector('[data-testid="primaryColumn"] a[role="link"][href$=/header_photo] img') +export const searchAvatarMetaSelector: () => LiveSelector = () => + querySelector('head > link[href^="https://twitter.com"] + meta[property="og:image"]') export const bioCardSelector = (singleMode = true) => querySelector( [ From c16d806275254f7b43ab45a80741e27fac30c365 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Sat, 9 Oct 2021 23:55:25 +0800 Subject: [PATCH 04/13] fix: failed to switch to the NFTs tab if the currently selected tab is loading (#4566) --- .../twitter.com/injection/EnhancedProfileTab.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/maskbook/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx b/packages/maskbook/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx index 047fcc3e3c12..c1ed4b9f9c32 100644 --- a/packages/maskbook/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx +++ b/packages/maskbook/src/social-network-adaptor/twitter.com/injection/EnhancedProfileTab.tsx @@ -1,6 +1,6 @@ import { MutationObserverWatcher } from '@dimensiondev/holoflows-kit' import { EnhancedProfileTab } from '../../../components/InjectedComponents/EnhancedProfileTab' -import { createReactRootShadowed, startWatch } from '../../../utils' +import { createReactRootShadowed, startWatch, untilElementAvailable } from '../../../utils' import { searchForegroundColorSelector, searchNewTweetButtonSelector, @@ -71,7 +71,7 @@ const useStyles = makeStyles()((theme, props) => ({ const EMPTY_STYLE = {} as CSSStyleDeclaration -function clear() { +async function clear() { const eleTab = searchProfileTabSelector().evaluate()?.querySelector('div') as Element if (!eleTab) return const style = window.getComputedStyle(eleTab) @@ -87,6 +87,7 @@ function clear() { if (eleEmpty) eleEmpty.style.display = 'none' // set display: none will change the height of the original element + await untilElementAvailable(searchProfileTabPageSelector()) const elePage = searchProfileTabPageSelector().evaluate() if (elePage) elePage.style.visibility = 'hidden' } From 5a04738257c5fe0b642fa80cb77857a4cafb92a1 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Sun, 10 Oct 2021 12:08:30 +0800 Subject: [PATCH 05/13] fix: opensea plugin didn't show sale end time if the end time is today or tomorrow(within 24 hrs) (#4560) --- .../src/plugins/Collectible/SNSAdaptor/Collectible.tsx | 1 - packages/maskbook/src/plugins/Collectible/apis/opensea.ts | 4 +--- packages/maskbook/src/plugins/Collectible/hooks/useAsset.ts | 5 ++++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/maskbook/src/plugins/Collectible/SNSAdaptor/Collectible.tsx b/packages/maskbook/src/plugins/Collectible/SNSAdaptor/Collectible.tsx index f6cbf36355dc..71fc9e886f48 100644 --- a/packages/maskbook/src/plugins/Collectible/SNSAdaptor/Collectible.tsx +++ b/packages/maskbook/src/plugins/Collectible/SNSAdaptor/Collectible.tsx @@ -185,7 +185,6 @@ export function Collectible(props: CollectibleProps) { ] const endDate = asset.value?.end_time - return ( <> diff --git a/packages/maskbook/src/plugins/Collectible/apis/opensea.ts b/packages/maskbook/src/plugins/Collectible/apis/opensea.ts index 0bf95ee0a94c..80a58148be00 100644 --- a/packages/maskbook/src/plugins/Collectible/apis/opensea.ts +++ b/packages/maskbook/src/plugins/Collectible/apis/opensea.ts @@ -60,9 +60,7 @@ export async function getAsset(tokenAddress: string, tokenId: string, chainId?: ).json() const endTime = head<{ closing_date: Date }>( - fetchResponse.orders.filter( - (item: { side: number; closing_extendable: boolean }) => item.side === 1 && item.closing_extendable, - ), + fetchResponse.orders.filter((item: { side: number; closing_extendable: boolean }) => item.side === 1), )?.closing_date return { diff --git a/packages/maskbook/src/plugins/Collectible/hooks/useAsset.ts b/packages/maskbook/src/plugins/Collectible/hooks/useAsset.ts index 351e210b0034..dbb29a7a550c 100644 --- a/packages/maskbook/src/plugins/Collectible/hooks/useAsset.ts +++ b/packages/maskbook/src/plugins/Collectible/hooks/useAsset.ts @@ -34,6 +34,7 @@ export function useAsset(provider: CollectibleProvider, token?: CollectibleToken new BigNumber(getOrderUnitPrice(b) ?? 0).toNumber(), ), ) + return { is_verified: ['approved', 'verified'].includes( openSeaResponse.collection?.safelist_request_status ?? '', @@ -69,7 +70,9 @@ export function useAsset(provider: CollectibleProvider, token?: CollectibleToken name: openSeaResponse.name ?? openSeaResponse.collection.name, collection_name: openSeaResponse.collection.name, animation_url: openSeaResponse.animation_url, - end_time: desktopOrder + end_time: openSeaResponse.endTime + ? new Date(openSeaResponse.endTime) + : desktopOrder ? toDate(Number.parseInt(desktopOrder.listingTime as unknown as string, 10)) : null, order_payment_tokens: desktopOrder?.paymentTokenContract From bcc3ba9f0190b5cec3cc3b45f4b9950fa71f67f1 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Sun, 10 Oct 2021 18:07:54 +0800 Subject: [PATCH 06/13] refactor: NFT wallet to use built-in hooks (#4555) * refactor: NFT wallet to use built-in hooks * refactor: ens type enum * fix: reply review --- .../components/InjectedComponents/EnhancedProfile.tsx | 6 +++--- packages/web3-shared/src/constants/index.ts | 7 +++++++ packages/web3-shared/src/hooks/index.ts | 1 + .../src/hooks}/useEthereumName.ts | 11 +++++++++-- 4 files changed, 20 insertions(+), 5 deletions(-) rename packages/{maskbook/src/social-network-adaptor/twitter.com/injection => web3-shared/src/hooks}/useEthereumName.ts (82%) diff --git a/packages/maskbook/src/components/InjectedComponents/EnhancedProfile.tsx b/packages/maskbook/src/components/InjectedComponents/EnhancedProfile.tsx index cce015b401ca..de4db8b93cb2 100644 --- a/packages/maskbook/src/components/InjectedComponents/EnhancedProfile.tsx +++ b/packages/maskbook/src/components/InjectedComponents/EnhancedProfile.tsx @@ -1,11 +1,11 @@ import { useStylesExtends } from '@masknet/shared' import { getMaskColor, makeStyles } from '@masknet/theme' -import { formatEthereumAddress, resolveAddressLinkOnExplorer, ChainId } from '@masknet/web3-shared' +import { formatEthereumAddress, resolveAddressLinkOnExplorer, ChainId, EthereumNameType } from '@masknet/web3-shared' import { Box, Link, Typography } from '@material-ui/core' import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined' import { useState, useEffect } from 'react' import { CollectibleListAddress } from '../../extension/options-page/DashboardComponents/CollectibleList' -import { useEthereumAddress } from '../../social-network-adaptor/twitter.com/injection/useEthereumName' +import { useEthereumAddress } from '@masknet/web3-shared' import { MaskMessage, useI18N } from '../../utils' import { useLocationChange } from '../../utils/hooks/useLocationChange' @@ -81,7 +81,7 @@ export function EnhancedProfilePage(props: EnhancedProfilePageProps) { href={resolveAddressLinkOnExplorer(ChainId.Mainnet, address)} target="_blank" rel="noopener noreferrer"> - {type === 'address' ? formatEthereumAddress(address, 4) : name} + {type === EthereumNameType.DEFAULT ? formatEthereumAddress(address, 4) : name} Date: Tue, 12 Oct 2021 16:09:14 +0800 Subject: [PATCH 07/13] style: scroll to auto on collectible tab (#4584) --- .../src/plugins/Collectible/SNSAdaptor/CollectibleTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/maskbook/src/plugins/Collectible/SNSAdaptor/CollectibleTab.tsx b/packages/maskbook/src/plugins/Collectible/SNSAdaptor/CollectibleTab.tsx index 46ce49caefab..ea6d7d01d026 100644 --- a/packages/maskbook/src/plugins/Collectible/SNSAdaptor/CollectibleTab.tsx +++ b/packages/maskbook/src/plugins/Collectible/SNSAdaptor/CollectibleTab.tsx @@ -7,7 +7,7 @@ const useStyles = makeStyles()({ width: '100%', height: '100%', borderRadius: 0, - overflow: 'scroll', + overflow: 'auto', }, }) From edf6803caea82196bb27fa4ac64253654c0198bb Mon Sep 17 00:00:00 2001 From: slfweb Date: Tue, 12 Oct 2021 12:31:55 +0200 Subject: [PATCH 08/13] feat: furucombo plugin (#4501) * feat(furucombo): initial config * feat(Furucombo): implement investable UI * feat(furucombo): implement investments UI * feat(furucombo): update plugin README * feat(furucombo): update furucombo README * feat(furucombo): fix build errors * feat(furucombo): review suggestions * feat(Furucombo): fix reviews * feat(furucombo): make changes from reviews * feat(furucombo): refactor rewardIcons displaying * feat(furucombo): fix check spells * feat(furucombo): refactor rewards icons * feat(furucombo): cache fetching pools * feat(furucombo): remove Dashboard and Worker * feat(furucombo): set fallback to default font --- cspell.json | 3 + packages/maskbook/src/locales/en-US.json | 10 + .../maskbook/src/plugin-infra/register.ts | 1 + .../maskbook/src/plugins/Furucombo/README.md | 20 ++ .../plugins/Furucombo/SNSAdaptor/index.tsx | 48 ++++ .../plugins/Furucombo/UI/FurucomboView.tsx | 160 +++++++++++ .../plugins/Furucombo/UI/InvestmentsView.tsx | 271 ++++++++++++++++++ .../src/plugins/Furucombo/UI/PoolView.tsx | 174 +++++++++++ .../src/plugins/Furucombo/apis/index.ts | 20 ++ .../maskbook/src/plugins/Furucombo/base.tsx | 18 ++ .../src/plugins/Furucombo/constants.ts | 5 + .../src/plugins/Furucombo/hooks/usePool.ts | 6 + .../maskbook/src/plugins/Furucombo/index.ts | 11 + .../src/plugins/Furucombo/messages.ts | 8 + .../plugins/Furucombo/resources/QuickIcon.tsx | 8 + .../Furucombo/resources/UnknownIcon.tsx | 16 ++ .../Furucombo/resources/WmaticIcon.tsx | 8 + .../src/plugins/Furucombo/services.ts | 1 + .../maskbook/src/plugins/Furucombo/types.tsx | 40 +++ .../maskbook/src/plugins/Furucombo/utils.ts | 27 ++ .../src/plugins/dHEDGE/UI/PoolStats.tsx | 2 +- .../maskbook/src/resources/FurucomboIcon.tsx | 139 +++++++++ 22 files changed, 995 insertions(+), 1 deletion(-) create mode 100644 packages/maskbook/src/plugins/Furucombo/README.md create mode 100644 packages/maskbook/src/plugins/Furucombo/SNSAdaptor/index.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/UI/FurucomboView.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/UI/InvestmentsView.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/UI/PoolView.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/apis/index.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/base.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/constants.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/hooks/usePool.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/index.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/messages.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/resources/QuickIcon.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/resources/UnknownIcon.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/resources/WmaticIcon.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/services.ts create mode 100644 packages/maskbook/src/plugins/Furucombo/types.tsx create mode 100644 packages/maskbook/src/plugins/Furucombo/utils.ts create mode 100644 packages/maskbook/src/resources/FurucomboIcon.tsx diff --git a/cspell.json b/cspell.json index 14c3d2e0151f..1fc70ca6a1ca 100644 --- a/cspell.json +++ b/cspell.json @@ -89,6 +89,7 @@ "fileservice", "formating", "fullfilled", + "furucombo", "gasnow", "geckoview", "gltf", @@ -102,6 +103,7 @@ "immer", "importmap", "imtokenv2", + "investables", "ipfs", "ipfsurl", "ittr", @@ -205,6 +207,7 @@ "webextension", "webm", "withdrawed", + "wmatic", "wnative", "xdai", "xlarge", diff --git a/packages/maskbook/src/locales/en-US.json b/packages/maskbook/src/locales/en-US.json index 38cf03a30906..3f0631bdff95 100644 --- a/packages/maskbook/src/locales/en-US.json +++ b/packages/maskbook/src/locales/en-US.json @@ -860,6 +860,16 @@ "plugin_unlockprotocol_submit_post": "Submit Post", "plugin_unlockprotocol_title": "Unlock Protocol", "plugin_unlockprotocol_server_error": "Some Server error occured, Please try again later.", + "plugin_furucombo_tab_pool": "Pool", + "plugin_furucombo_tab_investments": "All investments", + "plugin_furucombo_liquidity": "Liquidity", + "plugin_furucombo_annual_percentage_yield": "Apy", + "plugin_furucombo_rewards": "Rewards", + "plugin_furucombo_invest": "Invest", + "plugin_furucombo_pool_not_found": "Invalid pool address.", + "plugin_furucombo_smt_wrong": "Something went wrong!", + "plugin_furucombo_head_pools": "Pools", + "plugin_furucombo_head_action": "Action", "popups_welcome": "Welcome", "popups_wallet_token": "Token", "popups_wallet_go_back": "Go back", diff --git a/packages/maskbook/src/plugin-infra/register.ts b/packages/maskbook/src/plugin-infra/register.ts index 53b936f50116..33f1cc995107 100644 --- a/packages/maskbook/src/plugin-infra/register.ts +++ b/packages/maskbook/src/plugin-infra/register.ts @@ -18,5 +18,6 @@ import '../plugins/dHEDGE' import '../plugins/PoolTogether' import '../plugins/GoodGhosting' import '../plugins/UnlockProtocol' +import '../plugins/Furucombo' // import '../plugins/NFT' // import '../plugins/Airdrop' diff --git a/packages/maskbook/src/plugins/Furucombo/README.md b/packages/maskbook/src/plugins/Furucombo/README.md new file mode 100644 index 000000000000..8d27f3343d09 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/README.md @@ -0,0 +1,20 @@ +# Plugin: Furucombo + +This plugin fetch and show [Furucombo](https://furucombo.app) pools data. + +## Feature Set + +- [x] Detect Furucombo pool +- [x] Show pools list + +## Related projects + +- + +## Related discussion + +- + +## Pull requests + +- diff --git a/packages/maskbook/src/plugins/Furucombo/SNSAdaptor/index.tsx b/packages/maskbook/src/plugins/Furucombo/SNSAdaptor/index.tsx new file mode 100644 index 000000000000..d039635e74f3 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/SNSAdaptor/index.tsx @@ -0,0 +1,48 @@ +import { Suspense, useMemo } from 'react' +import { Plugin, usePostInfoDetails } from '@masknet/plugin-infra' +import { SnackbarContent } from '@material-ui/core' +import { base } from '../base' +import { extractTextFromTypedMessage } from '../../../protocols/typed-message' +import { parseURL } from '../../../utils/utils' +import MaskPluginWrapper from '../../MaskPluginWrapper' +import { FurucomboView } from '../UI/FurucomboView' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' + +const matchLink = /^https:\/\/furucombo.app\/invest\/(pool|farm)\/(137|1)\/(0x\w+)/ +const isFurucomboLink = (link: string): boolean => matchLink.test(link) + +const sns: Plugin.SNSAdaptor.Definition = { + ...base, + init(signal) {}, + DecryptedInspector: function Comp(props) { + const text = useMemo(() => extractTextFromTypedMessage(props.message), [props.message]) + const link = useMemo(() => parseURL(text.val || ''), [text.val]).find(isFurucomboLink) + if (!text.ok) return null + if (!link) return null + return + }, + PostInspector: function Component() { + const link = usePostInfoDetails + .postMetadataMentionedLinks() + .concat(usePostInfoDetails.postMentionedLinks()) + .find(isFurucomboLink) + if (!link) return null + return + }, +} + +function Renderer(props: React.PropsWithChildren<{ url: string }>) { + const [, category, chainId, address] = props.url.match(matchLink) ?? [] + + return ( + + }> + + + + + + ) +} + +export default sns diff --git a/packages/maskbook/src/plugins/Furucombo/UI/FurucomboView.tsx b/packages/maskbook/src/plugins/Furucombo/UI/FurucomboView.tsx new file mode 100644 index 000000000000..d60b197e94bb --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/UI/FurucomboView.tsx @@ -0,0 +1,160 @@ +import { makeStyles } from '@masknet/theme' +import { isSameAddress, useChainId } from '@masknet/web3-shared' +import { Card, CardContent, CardActions, Tabs, Tab, Typography, Link, Paper } from '@material-ui/core' +import { useState } from 'react' +import { FurucomboIcon } from '../../../resources/FurucomboIcon' +import { MaskTextIcon } from '../../../resources/MaskIcon' +import { useI18N } from '../../../utils/i18n-next-ui' +import { useFetchPools } from '../hooks/usePool' +import type { Investable } from '../types' +import { InvestmentsView } from './InvestmentsView' +import { PoolView } from './PoolView' + +const useStyles = makeStyles()((theme) => ({ + root: { + fontFamily: 'Muli,Helvetica,-apple-system,system-ui,"sans-serif"', + width: '100%', + boxShadow: 'none', + border: `solid 1px ${theme.palette.divider}`, + padding: 0, + }, + content: { + width: '100%', + height: 'var(--contentHeight)', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + padding: '0 !important', + }, + tabs: { + borderTop: `solid 1px ${theme.palette.divider}`, + borderBottom: `solid 1px ${theme.palette.divider}`, + backgroundColor: `#1b1b21`, + }, + tab: { + fontFamily: 'inherit', + color: 'white', + }, + footer: { + marginTop: -1, // merge duplicate borders + zIndex: 1, + position: 'relative', + borderTop: `solid 1px ${theme.palette.divider}`, + justifyContent: 'space-between', + }, + footnote: { + fontSize: 10, + marginRight: theme.spacing(1), + }, + footLink: { + cursor: 'pointer', + marginRight: theme.spacing(0.5), + '&:last-child': { + marginRight: 0, + }, + }, + footMenu: { + color: theme.palette.text.secondary, + fontSize: 10, + display: 'flex', + alignItems: 'center', + }, + footName: { + marginLeft: theme.spacing(0.5), + }, + maskbook: { + width: 40, + height: 10, + }, + furucombo: { + height: 16, + margin: theme.spacing(-0.5, 0.25), + }, +})) + +interface PoolViewProps { + address: string + category: string +} + +export function FurucomboView(props: PoolViewProps) { + const { t } = useI18N() + const { classes } = useStyles() + const [tabIndex, setTabIndex] = useState(0) + const currentChainId = useChainId() + + const { value, loading, error } = useFetchPools() + + if (loading) return Loading... + + if (error || !value) + return ( + + {t('plugin_furucombo_smt_wrong')} + + ) + + const { investables = [] } = value + + const investable = investables.find( + (investable: Investable) => + isSameAddress(investable.token.address, props.address) && + investable.chainId === currentChainId && + investable.category === props.category, + ) + + if (!investable) + return ( + + {t('plugin_furucombo_pool_not_found')} + + ) + + return ( + + + setTabIndex(newValue)}> + , + , + + + {tabIndex === 0 ? : null} + {tabIndex === 1 ? : null} + + + + + {t('plugin_powered_by')} + + + + + + Supported by + + + Furucombo + + + + + ) +} diff --git a/packages/maskbook/src/plugins/Furucombo/UI/InvestmentsView.tsx b/packages/maskbook/src/plugins/Furucombo/UI/InvestmentsView.tsx new file mode 100644 index 000000000000..65116d6c053c --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/UI/InvestmentsView.tsx @@ -0,0 +1,271 @@ +import * as React from 'react' +import { useTheme } from '@mui/material/styles' +import { apyFormatter, liquidityFormatter, shortenApy } from '../utils' +import { useI18N } from '../../../utils' +import { BASE_URL } from '../constants' +import { makeStyles } from '@masknet/theme' +import type { Investable } from '../types' +import { + Box, + Button, + Table, + TableSortLabel, + TableHead, + TableBody, + TableCell, + TableContainer, + TableFooter, + TablePagination, + TableRow, + Paper, + IconButton, +} from '@material-ui/core' + +import { + FirstPage as FirstPageIcon, + KeyboardArrowLeft, + KeyboardArrowRight, + LastPage as LastPageIcon, +} from '@mui/icons-material' + +type Order = 'asc' | 'desc' + +interface TablePaginationActionsProps { + count: number + page: number + rowsPerPage: number + onPageChange: (event: React.MouseEvent, newPage: number) => void +} + +function TablePaginationActions(props: TablePaginationActionsProps) { + const theme = useTheme() + const { count, page, rowsPerPage, onPageChange } = props + + const handleFirstPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, 0) + } + + const handleBackButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page - 1) + } + + const handleNextButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page + 1) + } + + const handleLastPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)) + } + + return ( + + + {theme.direction === 'rtl' ? : } + + + {theme.direction === 'rtl' ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="next page"> + {theme.direction === 'rtl' ? : } + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="last page"> + {theme.direction === 'rtl' ? : } + + + ) +} + +const useStyles = makeStyles()((theme) => ({ + root: { + fontFamily: 'inherit', + backgroundColor: '#232535', + color: 'white', + borderRadius: 0, + }, + head: { + borderBottom: '3px solid #393b4a', + textTransform: 'uppercase', + }, + row: { + color: 'white', + }, + cell: { + fontFamily: 'inherit', + + color: 'white', + borderBottom: '1px solid #393b4a', + }, + sort: { + color: 'white !important', + '& .MuiTableSortLabel-icon': { + fill: 'white', + }, + }, + pagination: { + '& .MuiIconButton-root, .MuiTablePagination-select, .MuiTablePagination-, .MuiTablePagination-selectLabel': { + color: 'white', + fontFamily: 'inherit', + }, + '& .MuiTablePagination-displayedRows, .MuiTablePagination-selectIcon': { + color: 'white', + fontFamily: 'inherit', + }, + '& .MuiTablePagination-menuItem': { + color: 'black', + fontFamily: 'inherit', + }, + }, + invest: { + maxWidth: theme.spacing(12), + fontFamily: 'inherit', + backgroundColor: 'white', + border: '1px solid #393b4a', + borderRadius: '6px', + color: '#212529', + '&:hover': { + backgroundColor: 'white', + color: '#212529', + border: '1px solid #393b4a', + }, + }, +})) + +interface InvestmentsProps { + investables: Investable[] +} + +enum Sortable { + apy = 'apy', + liquidity = 'liquidity', +} + +export function InvestmentsView(props: InvestmentsProps) { + const { classes } = useStyles() + const { t } = useI18N() + const investables = props.investables + const [page, setPage] = React.useState(0) + const [rowsPerPage, setRowsPerPage] = React.useState(5) + + const [order, setOrder] = React.useState('desc') + const [orderBy, setOrderBy] = React.useState('apy') + + investables.sort((a: object, b: object) => { + const orderByA = orderBy as keyof typeof a + const orderByB = orderBy as keyof typeof b + return order === 'asc' + ? parseFloat(a[orderByA]) - parseFloat(b[orderByB]) + : parseFloat(b[orderByB]) - parseFloat(a[orderByA]) + }) + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - investables.length) : 0 + + const handleChangePage = (event: React.MouseEvent | null, newPage: number) => { + setPage(newPage) + } + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)) + setPage(0) + } + + const handleRequestSort = (event: React.MouseEvent, property: string) => { + const isAsc = orderBy === property && order === 'asc' + setOrder(isAsc ? 'desc' : 'asc') + setOrderBy(property) + } + + return ( + + + + + + {t('plugin_furucombo_head_pools')} + + + handleRequestSort(e, 'apy')} + direction={orderBy === 'apy' ? order : 'asc'}> + {t('plugin_furucombo_annual_percentage_yield')} + + + + handleRequestSort(e, 'liquidity')} + direction={orderBy === 'liquidity' ? order : 'asc'}> + {t('plugin_furucombo_liquidity')} + + + + {t('plugin_furucombo_head_action')} + + + + + {(rowsPerPage > 0 + ? investables.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + : investables + ).map((row: Investable) => ( + + + {row.name} + + {shortenApy(apyFormatter(row.apy))} + {liquidityFormatter(row.liquidity, 1)} + + + + + ))} + {emptyRows > 0 && ( + + + + )} + + + + + + +
+
+ ) +} diff --git a/packages/maskbook/src/plugins/Furucombo/UI/PoolView.tsx b/packages/maskbook/src/plugins/Furucombo/UI/PoolView.tsx new file mode 100644 index 000000000000..e4202240115a --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/UI/PoolView.tsx @@ -0,0 +1,174 @@ +import { makeStyles } from '@masknet/theme' +import { Button, Divider, Grid, Typography, Container } from '@material-ui/core' +import { FurucomboIcon } from '../../../resources/FurucomboIcon' +import { useI18N } from '../../../utils' +import { BASE_URL } from '../constants' +import { QuickIcon } from '../resources/QuickIcon' +import { UnknownIcon } from '../resources/UnknownIcon' +import { WmaticIcon } from '../resources/WmaticIcon' + +import type { Angel, Investable, Token } from '../types' +import { apyFormatter, liquidityFormatter } from '../utils' + +const useStyles = makeStyles()((theme) => ({ + root: { + fontFamily: 'inherit', + backgroundColor: '#232535', + color: 'white', + padding: theme.spacing(2), + }, + deck: { + padding: theme.spacing(4), + gap: theme.spacing(4), + }, + meta: { + padding: theme.spacing(4), + }, + name: { + fontSize: 20, + fontFamily: 'inherit', + }, + protocol: { + color: '#9e9fa6', + fontFamily: 'inherit', + textTransform: 'capitalize', + }, + title: { + fontSize: 12, + fontFamily: 'inherit', + color: '#9e9fa6', + textTransform: 'uppercase', + }, + value: { + fontSize: 24, + fontFamily: 'inherit', + }, + divider: { + backgroundColor: '#393b4a', + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + invest: { + maxWidth: theme.spacing(12), + fontSize: 16, + fontFamily: 'inherit', + fontWeight: 400, + backgroundColor: 'white', + border: '1px solid #393b4a', + borderRadius: '6px', + color: '#212529', + paddingRight: theme.spacing(4), + paddingLeft: theme.spacing(4), + '&:hover': { + backgroundColor: 'white', + color: '#212529', + border: '1px solid #393b4a', + }, + }, + icons: { + height: 24, + width: 24, + }, + unknown: { + padding: theme.spacing(0.35), + height: 19, + width: 19, + }, + tooltip: { + backgroundColor: 'white', + }, +})) + +interface PoolProps { + investable: Investable +} + +export function PoolView(props: PoolProps) { + const { classes } = useStyles() + const { t } = useI18N() + const { + category, + chainId, + token: { address }, + name, + protocol, + liquidity, + apy, + angels, + } = props.investable + + const displayRewardIcon = (rewardToken: Token) => { + if (rewardToken.symbol === 'WMATIC') return + if (rewardToken.symbol === 'COMBO') return + if (rewardToken.symbol === 'QUICK') return + + return + } + + return ( +
+ + + + + {name} + + + {protocol} + + + + + + + + + + + + {t('plugin_furucombo_liquidity')} + + + {liquidityFormatter(liquidity, 1)} + + + + + + {t('plugin_furucombo_annual_percentage_yield')} + + + + {apyFormatter(apy)} + + + {category === 'farm' ? ( + + + {t('plugin_furucombo_rewards')} + + + {angels.map((angel: Angel) => ( + + {displayRewardIcon(angel.rewardToken)} + + ))} + + + ) : null} + +
+ ) +} diff --git a/packages/maskbook/src/plugins/Furucombo/apis/index.ts b/packages/maskbook/src/plugins/Furucombo/apis/index.ts new file mode 100644 index 000000000000..af8aefdb423c --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/apis/index.ts @@ -0,0 +1,20 @@ +import { API_KEY, API_URL } from '../constants' +import type { Investable } from '../types' + +export interface InvestablesData { + count: number + investables: Investable[] +} + +let cachedAt = Date.now() +let cachedResult: InvestablesData | undefined + +export async function fetchPools() { + if (cachedResult && Date.now() - cachedAt > 30 * 1000 /* cache for 30s */) return cachedResult + + const response = await fetch(API_URL, { headers: { 'x-api-key': API_KEY } }) + cachedResult = await response.json() + cachedAt = Date.now() + + return cachedResult +} diff --git a/packages/maskbook/src/plugins/Furucombo/base.tsx b/packages/maskbook/src/plugins/Furucombo/base.tsx new file mode 100644 index 000000000000..6371b0623a7d --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/base.tsx @@ -0,0 +1,18 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { PLUGIN_ID } from './constants' +import { FurucomboIcon } from '../../resources/FurucomboIcon' + +export const base: Plugin.Shared.Definition = { + ID: PLUGIN_ID, + icon: , + name: { fallback: 'Furucombo' }, + description: { + fallback: 'Furucombo is a tool built for end-users to optimize their DeFi strategy simply by drag and drop.', + }, + publisher: { name: { fallback: 'SebastianLF' }, link: 'https://github.com/SebastianLF' }, + enableRequirement: { + architecture: { app: false, web: true }, + networks: { type: 'opt-out', networks: {} }, + target: 'stable', + }, +} diff --git a/packages/maskbook/src/plugins/Furucombo/constants.ts b/packages/maskbook/src/plugins/Furucombo/constants.ts new file mode 100644 index 000000000000..6662e6422c92 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/constants.ts @@ -0,0 +1,5 @@ +export const PLUGIN_ID = 'app.furucombo' + +export const API_KEY = 'EryftBXrpN4ZG6dViNQtM3uNomPQpeyvgW6El9h0' +export const API_URL = 'https://api.furucombo.app/v1/investables' +export const BASE_URL = 'https://furucombo.app/invest' diff --git a/packages/maskbook/src/plugins/Furucombo/hooks/usePool.ts b/packages/maskbook/src/plugins/Furucombo/hooks/usePool.ts new file mode 100644 index 000000000000..c5cef8df8063 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/hooks/usePool.ts @@ -0,0 +1,6 @@ +import { useAsyncRetry } from 'react-use' +import { fetchPools } from '../apis' + +export function useFetchPools() { + return useAsyncRetry(() => fetchPools()) +} diff --git a/packages/maskbook/src/plugins/Furucombo/index.ts b/packages/maskbook/src/plugins/Furucombo/index.ts new file mode 100644 index 000000000000..83cbdd00cbaf --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/index.ts @@ -0,0 +1,11 @@ +import { registerPlugin } from '@masknet/plugin-infra' +import { base } from './base' + +registerPlugin({ + ...base, + SNSAdaptor: { + load: () => import('./SNSAdaptor'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./SNSAdaptor', () => hot(import('./SNSAdaptor'))), + }, +}) diff --git a/packages/maskbook/src/plugins/Furucombo/messages.ts b/packages/maskbook/src/plugins/Furucombo/messages.ts new file mode 100644 index 000000000000..e658aa02f3c6 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/messages.ts @@ -0,0 +1,8 @@ +import { createPluginMessage, PluginMessageEmitter, createPluginRPC } from '@masknet/plugin-infra' +import { PLUGIN_ID } from './constants' + +interface FurucomboMessages { + rpc: unknown +} +export const PluginFurucomboMessages: PluginMessageEmitter = createPluginMessage(PLUGIN_ID) +export const PluginFurucomboRPC = createPluginRPC(PLUGIN_ID, () => import('./services'), PluginFurucomboMessages.rpc) diff --git a/packages/maskbook/src/plugins/Furucombo/resources/QuickIcon.tsx b/packages/maskbook/src/plugins/Furucombo/resources/QuickIcon.tsx new file mode 100644 index 000000000000..72beb7c5eac2 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/resources/QuickIcon.tsx @@ -0,0 +1,8 @@ +import { Icon } from '@material-ui/core' +import type { FC } from 'react' + +export const QuickIcon: FC = (props) => ( + + WMATIC + +) diff --git a/packages/maskbook/src/plugins/Furucombo/resources/UnknownIcon.tsx b/packages/maskbook/src/plugins/Furucombo/resources/UnknownIcon.tsx new file mode 100644 index 000000000000..bdc558365f78 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/resources/UnknownIcon.tsx @@ -0,0 +1,16 @@ +import { SvgIcon, SvgIconProps } from '@material-ui/core' +import type { FC } from 'react' + +export const UnknownIcon: FC = (props) => ( + + + + + +) diff --git a/packages/maskbook/src/plugins/Furucombo/resources/WmaticIcon.tsx b/packages/maskbook/src/plugins/Furucombo/resources/WmaticIcon.tsx new file mode 100644 index 000000000000..367cef17e28a --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/resources/WmaticIcon.tsx @@ -0,0 +1,8 @@ +import { Icon } from '@material-ui/core' +import type { FC } from 'react' + +export const WmaticIcon: FC = (props) => ( + + WMATIC + +) diff --git a/packages/maskbook/src/plugins/Furucombo/services.ts b/packages/maskbook/src/plugins/Furucombo/services.ts new file mode 100644 index 000000000000..5914b704d67f --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/services.ts @@ -0,0 +1 @@ +export * from './apis' diff --git a/packages/maskbook/src/plugins/Furucombo/types.tsx b/packages/maskbook/src/plugins/Furucombo/types.tsx new file mode 100644 index 000000000000..dd1f521f2ee9 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/types.tsx @@ -0,0 +1,40 @@ +export interface Investable { + angels: Angel[] + apy: string + category: InvestableType + chainId: number + claims: Token[] + createdAt: string + id: number + liquidity: number + name: string + protocol: string + stakingToken: Token | null + strategies: Strategy[] + swapProtocol: string | null + token: Token + tokens: Token[] + updatedAt: string +} + +export interface Angel { + address: string + rewardToken: Token +} + +export interface Token { + address: string + decimals: number + name: string + symbol: string +} + +export interface Strategy { + locale: string + description: string +} + +export enum InvestableType { + farm = 'farm', + pool = 'pool', +} diff --git a/packages/maskbook/src/plugins/Furucombo/utils.ts b/packages/maskbook/src/plugins/Furucombo/utils.ts new file mode 100644 index 000000000000..af683140c9d3 --- /dev/null +++ b/packages/maskbook/src/plugins/Furucombo/utils.ts @@ -0,0 +1,27 @@ +export function liquidityFormatter(num: number, digits: number) { + const lookup = [ + { value: 1, symbol: '' }, + { value: 1e3, symbol: 'K' }, + { value: 1e6, symbol: 'M' }, + { value: 1e9, symbol: 'B' }, + { value: 1e12, symbol: 'T' }, + { value: 1e15, symbol: 'P' }, + { value: 1e18, symbol: 'E' }, + ] + const rx = /\.0+$|(\.\d*[1-9])0+$/ + const item = lookup + .slice() + .reverse() + .find(function (item) { + return num >= item.value + }) + return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0' +} + +export function shortenApy(num: string) { + return num.length > 9 ? num.slice(0, 9).concat('...') : num +} + +export function apyFormatter(num: string) { + return (+num * 100).toFixed(2) + '%' +} diff --git a/packages/maskbook/src/plugins/dHEDGE/UI/PoolStats.tsx b/packages/maskbook/src/plugins/dHEDGE/UI/PoolStats.tsx index d4f39ae7f25a..a219c43b8354 100644 --- a/packages/maskbook/src/plugins/dHEDGE/UI/PoolStats.tsx +++ b/packages/maskbook/src/plugins/dHEDGE/UI/PoolStats.tsx @@ -60,7 +60,7 @@ export function PoolStats(props: PoolStatsProps) { return (
- + diff --git a/packages/maskbook/src/resources/FurucomboIcon.tsx b/packages/maskbook/src/resources/FurucomboIcon.tsx new file mode 100644 index 000000000000..ddcea85c3f44 --- /dev/null +++ b/packages/maskbook/src/resources/FurucomboIcon.tsx @@ -0,0 +1,139 @@ +import { SvgIcon, SvgIconProps } from '@material-ui/core' +import type { FC } from 'react' + +export const FurucomboIcon: FC = (props) => ( + + COMBO_token + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) From 8745a740c49e738f35d0983a3e4e9993cbb552b8 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Tue, 12 Oct 2021 23:39:19 +0800 Subject: [PATCH 09/13] fix: buttons style (#4585) --- .../UnlockProtocol/SNSAdaptor/UnlockProtocolDialog.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/maskbook/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolDialog.tsx b/packages/maskbook/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolDialog.tsx index 0481a72ea6b2..e2af4664c172 100644 --- a/packages/maskbook/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolDialog.tsx +++ b/packages/maskbook/src/plugins/UnlockProtocol/SNSAdaptor/UnlockProtocolDialog.tsx @@ -10,6 +10,7 @@ import type { UnlockLocks } from '../types' import { PluginUnlockProtocolRPC } from '../messages' import { SelectRecipientsUnlockDialogUI } from './SelectRecipientsUnlockDialog' import { useCompositionContext } from '../../../components/CompositionDialog/CompositionContext' +import { makeStyles } from '@masknet/theme' interface UnlockProtocolDialogProps extends withClasses<'wrapper'> { open: boolean @@ -18,10 +19,17 @@ interface UnlockProtocolDialogProps extends withClasses<'wrapper'> { children?: React.ReactNode } +const useStyles = makeStyles()(() => ({ + actions: { + flexDirection: 'row', + }, +})) + export default function UnlockProtocolDialog(props: UnlockProtocolDialogProps) { const { t } = useI18N() const [open, setOpen] = useState(false) const address = useAccount() + const { classes } = useStyles() const [currentUnlockChain, setCurrentUnlockChain] = useState(useChainId()) const [currentUnlockPost, setCurrentUnlockPost] = useState('') const [currentUnlockTarget, setCurrentUnlockTarget] = useState(() => []) @@ -95,7 +103,7 @@ export default function UnlockProtocolDialog(props: UnlockProtocolDialogProps) { onChange={(e) => setCurrentUnlockPost(e.target.value)} /> - + setOpen(true)} /> setCurrentUnlockTarget([...currentUnlockTarget, item])} From 9cfa0248ed90b8920b5c61246a5fe11529ddd3cd Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Thu, 14 Oct 2021 17:32:08 +0800 Subject: [PATCH 10/13] fix: regex issue in matching address in bio (#4559) * fix: regex issue in matching address in bio * fix: reply review * fix: reply review * fix: revert code --- packages/web3-shared/src/hooks/useEthereumName.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/web3-shared/src/hooks/useEthereumName.ts b/packages/web3-shared/src/hooks/useEthereumName.ts index 4f6804626958..fb0ec31587ae 100644 --- a/packages/web3-shared/src/hooks/useEthereumName.ts +++ b/packages/web3-shared/src/hooks/useEthereumName.ts @@ -7,7 +7,8 @@ import { useResolveUNS } from './useResolveUNS' const ENS_RE = /\S{1,256}\.(eth|kred|xyz|luxe)\b/ const ENS_RE_FULL = new RegExp(`^${ENS_RE.source}$`) -const ADDRESS = /0x[\dA-Fa-f]{40}/ +const ADDRESS_FULL = /0x\w+/ +const ADDRESS = /^0x[\dA-Fa-f]{40}$/ export function useEthereumName(nickname: string, twitterId: string, bio: string) { const [ethereumName, setEthereumName] = useState('') @@ -39,8 +40,8 @@ export function useEthereumAddress(nickname: string, twitterId: string, bio: str useEffect(() => { setAddress('') - const matched = bio.match(ADDRESS) - if (matched) setAddress(matched[0]) + const matched = bio.match(ADDRESS_FULL) + if (matched && ADDRESS.test(matched[0])) setAddress(matched[0]) }, [bio]) const isLoading = loadingAddressENS || loadingAddressUNS || loadingAddressNames From 0f8f5d5828032a12b7b19a7b63c336e6e4beb4d9 Mon Sep 17 00:00:00 2001 From: Hancheng Zhou Date: Fri, 15 Oct 2021 15:15:17 +0800 Subject: [PATCH 11/13] fix: snapshot getScores api (#4599) --- .../Snapshot/SNSAdaptor/hooks/usePower.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/maskbook/src/plugins/Snapshot/SNSAdaptor/hooks/usePower.ts b/packages/maskbook/src/plugins/Snapshot/SNSAdaptor/hooks/usePower.ts index 624b97968e4e..893ca9e4632e 100644 --- a/packages/maskbook/src/plugins/Snapshot/SNSAdaptor/hooks/usePower.ts +++ b/packages/maskbook/src/plugins/Snapshot/SNSAdaptor/hooks/usePower.ts @@ -3,6 +3,7 @@ import { useAccount, useBlockNumber } from '@masknet/web3-shared' import { PluginSnapshotRPC } from '../../messages' import type { ProposalIdentifier } from '../../types' import { useProposal } from './useProposal' +import { mapKeys } from 'lodash-es' export function usePower(identifier: ProposalIdentifier) { const { @@ -13,14 +14,19 @@ export function usePower(identifier: ProposalIdentifier) { const blockNumber = useBlockNumber() return useAsyncRetry(async () => { if (!account) return 0 - const scores = await PluginSnapshotRPC.getScores( - message, - [account], - blockNumber, - proposal.network, - identifier.space, - proposal.strategies, + return ( + await PluginSnapshotRPC.getScores( + message, + [account], + blockNumber, + proposal.network, + identifier.space, + proposal.strategies, + ) ) - return scores[0]![account]! + .map((v) => mapKeys(v, (_value, key) => key.toLowerCase()) as { [x: string]: number }) + .reduce((acc, cur) => { + return acc + (cur[account.toLowerCase()] ?? 0) + }, 0) }, [blockNumber, account]) } From 5ac83728688b2db940256c82a6502b2c1ca0cf11 Mon Sep 17 00:00:00 2001 From: lelenei <72531217+lelenei@users.noreply.github.com> Date: Sat, 16 Oct 2021 21:57:58 +0800 Subject: [PATCH 12/13] fix: the dropdown menu goes out of screen (#4600) --- .../Trader/SNSAdaptor/trader/FootnoteMenu.tsx | 35 +++++++++++-------- packages/maskbook/src/utils/hooks/useMenu.tsx | 7 +++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/maskbook/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx b/packages/maskbook/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx index 438505583880..2ce88adacbc8 100644 --- a/packages/maskbook/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx +++ b/packages/maskbook/src/plugins/Trader/SNSAdaptor/trader/FootnoteMenu.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react' import { Typography, MenuItem, Link } from '@material-ui/core' import { makeStyles } from '@masknet/theme' -import { ShadowRootMenu } from '../../../../utils/shadow-root/ShadowRootComponents' +import { useMenu } from '../../../../utils' const useStyles = makeStyles()((theme) => ({ link: { @@ -31,29 +30,37 @@ export function FootnoteMenu(props: FootnoteMenuProps) { const { children, options, selectedIndex = -1, onChange } = props const { classes } = useStyles() - const [anchorEl, setAnchorEl] = useState(null) - const onOpen = (event: React.MouseEvent) => setAnchorEl(event.currentTarget) const onSelect = (option: FootnoteMenuOption) => { onChange?.(option) - onClose() } - const onClose = () => setAnchorEl(null) + const [menu, openMenu] = useMenu( + options.map((x, i) => ( + onSelect(x)}> + {x.name} + + )), + false, + { + anchorOrigin: { + vertical: 'top', + horizontal: 'left', + }, + transformOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, + }, + ) return ( <> - + {options[selectedIndex]?.name} {children} - - {options.map((x, i) => ( - onSelect(x)}> - {x.name} - - ))} - + {menu} ) } diff --git a/packages/maskbook/src/utils/hooks/useMenu.tsx b/packages/maskbook/src/utils/hooks/useMenu.tsx index 4d8cdfccffd8..7e2adcd33c5b 100644 --- a/packages/maskbook/src/utils/hooks/useMenu.tsx +++ b/packages/maskbook/src/utils/hooks/useMenu.tsx @@ -1,11 +1,13 @@ import { SyntheticEvent, cloneElement, isValidElement, useCallback, useRef, useState, createElement } from 'react' -import { Menu } from '@material-ui/core' +import { Menu, PopoverOrigin } from '@material-ui/core' import type { MenuListProps, PaperProps } from '@material-ui/core' import { ShadowRootMenu } from '../shadow-root/ShadowRootComponents' interface MenuProps { paperProps?: PaperProps menuListProps?: MenuListProps + anchorOrigin?: PopoverOrigin + transformOrigin?: PopoverOrigin } /** @@ -31,6 +33,9 @@ export function useMenu( anchorEl: anchorElRef.current, onClose: close, onClick: close, + + anchorOrigin: props?.anchorOrigin, + transformOrigin: props?.transformOrigin, }, elements?.map((element, key) => isValidElement(element) ? cloneElement(element, { ...element.props, key }) : element, From 5c61f7a66337124d53d9711ac53a65dfdffb4c45 Mon Sep 17 00:00:00 2001 From: guanbinrui <52657989+guanbinrui@users.noreply.github.com> Date: Tue, 12 Oct 2021 23:40:14 +0800 Subject: [PATCH 13/13] feat: MaskBox Integration (#4450) * chore: scaffolding * chore(ui): impl UI components * chore(ui): add placeholder component * chore(ui): the draw dialog * chore: contract interaction * fix: the total num of tokens in a box * fix: draw dialog numbers * fix(ui): the tab * refactor: draw dialog & draw result dialog * fix: gas settings * chore: add rss-next * chore: fetch box metadata * fix: pkg error * fix: estimate gas error * fix: box state * fix: service bg * fix: UI errors * chore: dev link --- cspell.json | 4 +- packages/backup-format/package.json | 1 - .../components/Transfer/TransferERC20.tsx | 6 +- packages/maskbook/package.json | 1 + .../src/components/shared/TokenPrice.tsx | 5 +- .../DashboardComponents/ActionButton.tsx | 2 +- .../Wallet/GasSetting/GasSetting1559.tsx | 12 +- .../Wallet/GasSetting/Prior1559GasSetting.tsx | 10 +- .../Wallet/Transfer/Prior1559Transfer.tsx | 4 +- .../pages/Wallet/Transfer/Transfer1559.tsx | 6 +- packages/maskbook/src/locales/en-US.json | 10 +- packages/maskbook/src/locales/ko-KR.json | 8 +- packages/maskbook/src/locales/qya-AA.json | 8 +- .../maskbook/src/plugin-infra/register.ts | 1 + .../ITO/SNSAdaptor/CompositionDialog.tsx | 1 + .../SNSAdaptor/components/ArticlesTab.tsx | 72 +++ .../SNSAdaptor/components/DetailsTab.tsx | 73 +++ .../SNSAdaptor/components/DrawDialog.tsx | 272 +++++++++++ .../components/DrawResultDialog.tsx | 72 +++ .../SNSAdaptor/components/PreviewCard.tsx | 219 +++++++++ .../SNSAdaptor/components/TokenCard.tsx | 45 ++ .../src/plugins/MaskBox/SNSAdaptor/index.tsx | 53 +++ .../src/plugins/MaskBox/Worker/index.ts | 9 + .../src/plugins/MaskBox/apis/index.ts | 1 + .../maskbook/src/plugins/MaskBox/apis/rss3.ts | 22 + packages/maskbook/src/plugins/MaskBox/base.ts | 18 + .../maskbook/src/plugins/MaskBox/constants.ts | 3 + .../MaskBox/helpers/formatCountdown.ts | 32 ++ .../src/plugins/MaskBox/hooks/useContext.ts | 274 +++++++++++ .../src/plugins/MaskBox/hooks/useHeartBit.ts | 11 + .../MaskBox/hooks/useMaskBoxContract.ts | 9 + .../hooks/useMaskBoxCreationSuccessEvent.ts | 23 + .../plugins/MaskBox/hooks/useMaskBoxInfo.ts | 10 + .../MaskBox/hooks/useMaskBoxMetadata.ts | 10 + .../hooks/useMaskBoxPurchasedTokens.ts | 10 + .../MaskBox/hooks/useMaskBoxTokensForSale.ts | 10 + .../MaskBox/hooks/useOpenBoxGasLimit.ts | 12 + .../MaskBox/hooks/useOpenBoxTransaction.ts | 31 ++ .../maskbook/src/plugins/MaskBox/index.ts | 16 + .../src/plugins/MaskBox/locales/en-US.json | 4 + .../src/plugins/MaskBox/locales/index.ts | 13 + .../src/plugins/MaskBox/locales/zh-TW.json | 4 + .../maskbook/src/plugins/MaskBox/messages.ts | 9 + .../maskbook/src/plugins/MaskBox/services.ts | 1 + packages/maskbook/src/plugins/MaskBox/type.ts | 61 +++ .../RedPacket/SNSAdaptor/RedPacketDialog.tsx | 1 - .../SNSAdaptor/RedPacketERC721Form.tsx | 2 +- .../GasSettingDialog/GasSettingBar.tsx | 93 ++++ .../UI/EthereumERC20TokenApprovedBoundary.tsx | 28 +- .../EthereumERC721TokenApprovedBoundary.tsx | 61 ++- .../UI/EthereumWalletConnectedBoundary.tsx | 17 +- packages/web3-constants/data/mask-box.json | 72 +++ .../web3-constants/data/nft-red-packet.json | 6 +- .../data/space-station-galaxy.json | 6 +- packages/web3-contracts/abis/MaskBox.json | 436 ++++++++++++++++++ packages/web3-contracts/types/MaskBox.d.ts | 165 +++++++ packages/web3-shared/src/constants/index.ts | 11 +- packages/web3-shared/src/hooks/index.ts | 3 + packages/web3-shared/src/hooks/useContract.ts | 2 +- .../src/hooks/useERC721ContractDetailed.ts | 6 +- .../hooks/useERC721ContractIsApproveForAll.ts | 2 +- ...eERC721ContractSetApproveForAllCallback.ts | 2 +- .../web3-shared/src/hooks/useProviderType.ts | 5 + .../src/hooks/useTransactionCallback.ts | 84 ++++ pnpm-lock.yaml | 157 +++++-- 65 files changed, 2531 insertions(+), 106 deletions(-) create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/ArticlesTab.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DetailsTab.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawDialog.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawResultDialog.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/PreviewCard.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/SNSAdaptor/index.tsx create mode 100644 packages/maskbook/src/plugins/MaskBox/Worker/index.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/apis/index.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/apis/rss3.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/base.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/constants.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/helpers/formatCountdown.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useContext.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useHeartBit.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxContract.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxCreationSuccessEvent.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxInfo.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxMetadata.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxPurchasedTokens.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxTokensForSale.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxGasLimit.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxTransaction.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/index.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/locales/en-US.json create mode 100644 packages/maskbook/src/plugins/MaskBox/locales/index.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/locales/zh-TW.json create mode 100644 packages/maskbook/src/plugins/MaskBox/messages.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/services.ts create mode 100644 packages/maskbook/src/plugins/MaskBox/type.ts create mode 100644 packages/maskbook/src/plugins/Wallet/SNSAdaptor/GasSettingDialog/GasSettingBar.tsx create mode 100644 packages/web3-constants/data/mask-box.json create mode 100644 packages/web3-contracts/abis/MaskBox.json create mode 100644 packages/web3-contracts/types/MaskBox.d.ts create mode 100644 packages/web3-shared/src/hooks/useProviderType.ts create mode 100644 packages/web3-shared/src/hooks/useTransactionCallback.ts diff --git a/cspell.json b/cspell.json index 1fc70ca6a1ca..fcefb005182e 100644 --- a/cspell.json +++ b/cspell.json @@ -295,7 +295,9 @@ "gamification", "Defi", "powah", - "Switcher" + "Switcher", + "drawed", + "DRAWED" ], "ignoreRegExpList": ["/@servie/"], "ignorePaths": [ diff --git a/packages/backup-format/package.json b/packages/backup-format/package.json index 044dc2e4234e..ff99b9a3c9fc 100644 --- a/packages/backup-format/package.json +++ b/packages/backup-format/package.json @@ -1,6 +1,5 @@ { "name": "@masknet/backup-format", - "version": "0.0.0", "private": true, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/dashboard/src/pages/Wallets/components/Transfer/TransferERC20.tsx b/packages/dashboard/src/pages/Wallets/components/Transfer/TransferERC20.tsx index 35f54d8284c8..e0a8236cceba 100644 --- a/packages/dashboard/src/pages/Wallets/components/Transfer/TransferERC20.tsx +++ b/packages/dashboard/src/pages/Wallets/components/Transfer/TransferERC20.tsx @@ -1,6 +1,8 @@ +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import BigNumber from 'bignumber.js' +import { EthereumAddress } from 'wallet.ts' import { MaskTextField } from '@masknet/theme' import { Box, Button, Stack } from '@material-ui/core' -import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { EthereumTokenType, FungibleTokenDetailed, @@ -12,11 +14,9 @@ import { useGasPrice, useTokenTransferCallback, } from '@masknet/web3-shared' -import BigNumber from 'bignumber.js' import { TokenAmountPanel } from '@masknet/shared' import { SelectTokenDialog } from '../SelectTokenDialog' import { useDashboardI18N } from '../../../../locales' -import { EthereumAddress } from 'wallet.ts' interface TransferERC20Props { token: FungibleTokenDetailed diff --git a/packages/maskbook/package.json b/packages/maskbook/package.json index 59e3eaacfc74..9d97ab96b693 100644 --- a/packages/maskbook/package.json +++ b/packages/maskbook/package.json @@ -105,6 +105,7 @@ "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "remarkable": "^2.0.1", + "rss3-next": "0.6.17", "safari-14-idb-fix": "^2.0.3", "scrypt-js": "^3.0.1", "socket.io-client": "2.4.0", diff --git a/packages/maskbook/src/components/shared/TokenPrice.tsx b/packages/maskbook/src/components/shared/TokenPrice.tsx index 9c4b2dd76117..94b4a80bce16 100644 --- a/packages/maskbook/src/components/shared/TokenPrice.tsx +++ b/packages/maskbook/src/components/shared/TokenPrice.tsx @@ -1,11 +1,12 @@ import type { FC, HTMLProps } from 'react' +import BigNumber from 'bignumber.js' import { ChainId, CurrencyType } from '@masknet/web3-shared' import { useTokenPrice } from '../../plugins/Wallet/hooks/useTokenPrice' interface TokenPriceProps extends Omit, 'children'> { chainId: ChainId contractAddress: string | undefined - amount: number + amount: BigNumber.Value currencyType?: CurrencyType } @@ -17,5 +18,5 @@ export const TokenPrice: FC = ({ ...rest }) => { const price = useTokenPrice(chainId, contractAddress, currencyType) - return ${(price * amount).toFixed(2)} + return ${new BigNumber(amount).multipliedBy(price).toFixed(2)} } diff --git a/packages/maskbook/src/extension/options-page/DashboardComponents/ActionButton.tsx b/packages/maskbook/src/extension/options-page/DashboardComponents/ActionButton.tsx index ddda4aa6db0c..3cb43f23d446 100644 --- a/packages/maskbook/src/extension/options-page/DashboardComponents/ActionButton.tsx +++ b/packages/maskbook/src/extension/options-page/DashboardComponents/ActionButton.tsx @@ -58,7 +58,7 @@ export function DebounceButton(_props: DebounceButtonProps) { ) } -interface ActionButtonProps extends ButtonProps { +export interface ActionButtonProps extends ButtonProps { width?: number | string loading?: boolean component?: keyof JSX.IntrinsicElements | React.ComponentType diff --git a/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/GasSetting1559.tsx b/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/GasSetting1559.tsx index 139386e0700d..29f0dea512b0 100644 --- a/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/GasSetting1559.tsx +++ b/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/GasSetting1559.tsx @@ -56,15 +56,15 @@ const useStyles = makeStyles()((theme) => ({ justifyContent: 'center', }, label: { - color: '#1C68F3', + color: theme.palette.primary.main, fontSize: 12, lineHeight: '16px', margin: '10px 0', }, selected: { - backgroundColor: '#1C68F3', + backgroundColor: theme.palette.primary.main, '& > *': { - color: '#ffffff!important', + color: theme.palette.primary.contrastText, }, }, button: { @@ -132,14 +132,14 @@ export const GasSetting1559 = memo(() => { .object({ gasLimit: zod .string() - .min(1, t('wallet_transfer_error_gasLimit_absence')) + .min(1, t('wallet_transfer_error_gas_limit_absence')) .refine( (gasLimit) => new BigNumber(gasLimit).isGreaterThanOrEqualTo(gas), `Gas limit must be at least ${gas}.`, ), maxPriorityFeePerGas: zod .string() - .min(1, t('wallet_transfer_error_maxPriority_fee_absence')) + .min(1, t('wallet_transfer_error_max_priority_fee_absence')) .refine( (value) => new BigNumber(value).isPositive(), t('wallet_transfer_error_max_priority_gas_fee_positive'), @@ -160,7 +160,7 @@ export const GasSetting1559 = memo(() => { ), maxFeePerGas: zod .string() - .min(1, t('wallet_transfer_error_maxFee_absence')) + .min(1, t('wallet_transfer_error_max_fee_absence')) .refine( (value) => new BigNumber(value).isGreaterThanOrEqualTo(gasNow?.slow?.suggestedMaxFeePerGas ?? 0), diff --git a/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/Prior1559GasSetting.tsx b/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/Prior1559GasSetting.tsx index be6436e70198..0d364557fb96 100644 --- a/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/Prior1559GasSetting.tsx +++ b/packages/maskbook/src/extension/popups/pages/Wallet/GasSetting/Prior1559GasSetting.tsx @@ -62,15 +62,15 @@ const useStyles = makeStyles()((theme) => ({ justifyContent: 'center', }, label: { - color: '#1C68F3', + color: theme.palette.primary.main, fontSize: 12, lineHeight: '16px', margin: '10px 0', }, selected: { - backgroundColor: '#1C68F3', + backgroundColor: theme.palette.primary.main, '& > *': { - color: '#ffffff!important', + color: theme.palette.primary.contrastText, }, }, button: { @@ -136,12 +136,12 @@ export const Prior1559GasSetting = memo(() => { return zod.object({ gasLimit: zod .string() - .min(1, t('wallet_transfer_error_gasLimit_absence')) + .min(1, t('wallet_transfer_error_gas_limit_absence')) .refine( (gasLimit) => new BigNumber(gasLimit).isGreaterThanOrEqualTo(gas), `Gas limit must be at least ${gas}.`, ), - gasPrice: zod.string().min(1, t('wallet_transfer_error_gasPrice_absence')), + gasPrice: zod.string().min(1, t('wallet_transfer_error_gas_price_absence')), }) }, [gas]) diff --git a/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx b/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx index 1b77241e85f8..2491d17aea62 100644 --- a/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx +++ b/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Prior1559Transfer.tsx @@ -155,12 +155,12 @@ export const Prior1559Transfer = memo(({ selectedAsset, }, t('wallet_transfer_error_insufficient_balance', { token: selectedAsset?.token.symbol })), gasLimit: zod .string() - .min(1, t('wallet_transfer_error_gasLimit_absence')) + .min(1, t('wallet_transfer_error_gas_limit_absence')) .refine( (gasLimit) => new BigNumber(gasLimit).isGreaterThanOrEqualTo(minGasLimitContext), ` Gas limit must be at least ${minGasLimitContext}.`, ), - gasPrice: zod.string().min(1, t('wallet_transfer_error_gasPrice_absence')), + gasPrice: zod.string().min(1, t('wallet_transfer_error_gas_price_absence')), }) }, [selectedAsset, minGasLimitContext]) diff --git a/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx b/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx index 99b55909fa73..89e4dcef0c6e 100644 --- a/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx +++ b/packages/maskbook/src/extension/popups/pages/Wallet/Transfer/Transfer1559.tsx @@ -177,14 +177,14 @@ export const Transfer1559 = memo(({ selectedAsset, openAssetM }, t('wallet_transfer_error_insufficient_balance', { token: selectedAsset?.token.symbol })), gasLimit: zod .string() - .min(1, t('wallet_transfer_error_gasLimit_absence')) + .min(1, t('wallet_transfer_error_gas_limit_absence')) .refine( (gasLimit) => new BigNumber(gasLimit).isGreaterThanOrEqualTo(minGasLimitContext), ` Gas limit must be at least ${minGasLimitContext}.`, ), maxPriorityFeePerGas: zod .string() - .min(1, t('wallet_transfer_error_maxPriority_fee_absence')) + .min(1, t('wallet_transfer_error_max_priority_fee_absence')) .refine( (value) => new BigNumber(value).isPositive(), t('wallet_transfer_error_max_priority_gas_fee_positive'), @@ -205,7 +205,7 @@ export const Transfer1559 = memo(({ selectedAsset, openAssetM ), maxFeePerGas: zod .string() - .min(1, t('wallet_transfer_error_maxFee_absence')) + .min(1, t('wallet_transfer_error_max_fee_absence')) .refine( (value) => new BigNumber(value).isGreaterThanOrEqualTo( diff --git a/packages/maskbook/src/locales/en-US.json b/packages/maskbook/src/locales/en-US.json index 3f0631bdff95..4b014fe68ccc 100644 --- a/packages/maskbook/src/locales/en-US.json +++ b/packages/maskbook/src/locales/en-US.json @@ -316,10 +316,10 @@ "wallet_transfer_error_address_absence": "Enter recipient address", "wallet_transfer_error_invalid_address": "Invalid recipient address", "wallet_transfer_error_insufficient_balance": "Insufficient {{symbol}} balance", - "wallet_transfer_error_gasPrice_absence": "Enter a gas price", - "wallet_transfer_error_gasLimit_absence": "Enter a gas limit", - "wallet_transfer_error_maxFee_absence": "Enter a max fee", - "wallet_transfer_error_maxPriority_fee_absence": "Enter a max priority fee", + "wallet_transfer_error_gas_price_absence": "Enter a gas price", + "wallet_transfer_error_gas_limit_absence": "Enter a gas limit", + "wallet_transfer_error_max_fee_absence": "Enter a max fee", + "wallet_transfer_error_max_priority_fee_absence": "Enter a max priority fee", "wallet_transfer_error_max_fee_too_low": "Max fee too low for network conditions.", "wallet_transfer_error_max_fee_too_high": "Max fee is higher than necessary", "wallet_transfer_error_max_priority_gas_fee_positive": "Max priority fee must be greater than 0 GWEI", @@ -902,7 +902,7 @@ "popups_wallet_gas_fee_settings_high": "High", "popups_wallet_gas_fee_settings_usd": " ≈ ${{usd}}", "popups_wallet_gas_fee_settings_or": "or", - "popups_wallet_gas_fee_settings_gas_limit": "Gas limit", + "popups_wallet_gas_fee_settings_gas_limit": "Gas Limit", "popups_wallet_gas_fee_settings_max_priority_fee": "Max priority fee", "popups_wallet_gas_fee_settings_max_fee": "Max fee", "popups_wallet_signature_request": "Signature request", diff --git a/packages/maskbook/src/locales/ko-KR.json b/packages/maskbook/src/locales/ko-KR.json index 21f276fe73dc..660863b30b98 100644 --- a/packages/maskbook/src/locales/ko-KR.json +++ b/packages/maskbook/src/locales/ko-KR.json @@ -307,10 +307,10 @@ "wallet_transfer_error_address_absence": "받는 주소 입력", "wallet_transfer_error_invalid_address": "무효한 받는 주소", "wallet_transfer_error_insufficient_balance": "{{symbol}} 잔액 부족", - "wallet_transfer_error_gasPrice_absence": "가스비 입력", - "wallet_transfer_error_gasLimit_absence": "기스비 한도 입력", - "wallet_transfer_error_maxFee_absence": "최대 가스비 입력", - "wallet_transfer_error_maxPriority_fee_absence": "최대 우선 가스비 입력", + "wallet_transfer_error_gas_price_absence": "가스비 입력", + "wallet_transfer_error_gas_limit_absence": "기스비 한도 입력", + "wallet_transfer_error_max_fee_absence": "최대 가스비 입력", + "wallet_transfer_error_max_priority_fee_absence": "최대 우선 가스비 입력", "wallet_transfer_error_max_fee_too_low": "현재 네트워크 컨디션에서 최대 가스비가 너무 낮습니다.", "wallet_transfer_error_max_fee_too_high": "최대 가스비는 필요한 것보다 높습니다.", "wallet_transfer_error_max_priority_gas_fee_positive": "최대 우선 가스비는 0 GWEI보다 높아야 합니다", diff --git a/packages/maskbook/src/locales/qya-AA.json b/packages/maskbook/src/locales/qya-AA.json index bf4fd97e62db..e7f60900240c 100644 --- a/packages/maskbook/src/locales/qya-AA.json +++ b/packages/maskbook/src/locales/qya-AA.json @@ -316,10 +316,10 @@ "wallet_transfer_error_address_absence": "crwdns4545:0crwdne4545:0", "wallet_transfer_error_invalid_address": "crwdns4547:0crwdne4547:0", "wallet_transfer_error_insufficient_balance": "crwdns7961:0{{symbol}}crwdne7961:0", - "wallet_transfer_error_gasPrice_absence": "crwdns8035:0crwdne8035:0", - "wallet_transfer_error_gasLimit_absence": "crwdns8037:0crwdne8037:0", - "wallet_transfer_error_maxFee_absence": "crwdns8039:0crwdne8039:0", - "wallet_transfer_error_maxPriority_fee_absence": "crwdns8041:0crwdne8041:0", + "wallet_transfer_error_gas_price_absence": "crwdns8035:0crwdne8035:0", + "wallet_transfer_error_gas_limit_absence": "crwdns8037:0crwdne8037:0", + "wallet_transfer_error_max_fee_absence": "crwdns8039:0crwdne8039:0", + "wallet_transfer_error_max_priority_fee_absence": "crwdns8041:0crwdne8041:0", "wallet_transfer_error_max_fee_too_low": "crwdns8043:0crwdne8043:0", "wallet_transfer_error_max_fee_too_high": "crwdns8045:0crwdne8045:0", "wallet_transfer_error_max_priority_gas_fee_positive": "crwdns8047:0crwdne8047:0", diff --git a/packages/maskbook/src/plugin-infra/register.ts b/packages/maskbook/src/plugin-infra/register.ts index 33f1cc995107..3b803f8ef464 100644 --- a/packages/maskbook/src/plugin-infra/register.ts +++ b/packages/maskbook/src/plugin-infra/register.ts @@ -19,5 +19,6 @@ import '../plugins/PoolTogether' import '../plugins/GoodGhosting' import '../plugins/UnlockProtocol' import '../plugins/Furucombo' +import '../plugins/MaskBox' // import '../plugins/NFT' // import '../plugins/Airdrop' diff --git a/packages/maskbook/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx b/packages/maskbook/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx index 3c80d52fdf84..1e41cb2abe7c 100644 --- a/packages/maskbook/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx +++ b/packages/maskbook/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx @@ -230,6 +230,7 @@ export function CompositionDialog(props: CompositionDialogProps) { }), }) }, [fillState, poolSettings, setTransactionDialog]) + //#endregion useEffect(() => { if (!ITO2_CONTRACT_ADDRESS) onClose() diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/ArticlesTab.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/ArticlesTab.tsx new file mode 100644 index 000000000000..6488cdd286ee --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/ArticlesTab.tsx @@ -0,0 +1,72 @@ +import { Box, Typography } from '@material-ui/core' +import { makeStyles } from '@masknet/theme' +import type { BoxInfo, BoxMetadata } from '../../type' +import { MaskSharpIconOfSize } from '../../../../resources/MaskIcon' + +const useStyles = makeStyles()((theme) => ({ + main: {}, + body: { + width: '100%', + height: 360, + overflow: 'hidden', + borderRadius: 8, + boxSizing: 'border-box', + border: `solid 1px ${theme.palette.divider}`, + }, + footer: { + margin: theme.spacing(2.75, 0), + }, + hero: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: 360, + objectFit: 'scale-down', + }, + name: { + whiteSpace: 'nowrap', + maxWidth: '50%', + display: 'inline-block', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + icon: { + color: theme.palette.mode === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(15, 20, 25)', + width: 64, + height: 64, + opacity: 0.1, + }, +})) + +export interface ArticlesTabProps { + boxInfo: BoxInfo + boxMetadata?: BoxMetadata +} + +export function ArticlesTab(props: ArticlesTabProps) { + const { boxInfo, boxMetadata } = props + const { classes } = useStyles() + + return ( + + + {boxMetadata?.cover ? ( + + ) : ( + + + + )} + + + + {boxInfo.name} + + + {boxInfo.remaining}/{boxInfo.total} + + + + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DetailsTab.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DetailsTab.tsx new file mode 100644 index 000000000000..454aad59ff1c --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DetailsTab.tsx @@ -0,0 +1,73 @@ +import { Box, Typography } from '@material-ui/core' +import { makeStyles } from '@masknet/theme' +import type { BoxInfo, BoxMetadata } from '../../type' + +const useStyles = makeStyles()((theme) => ({ + main: { + height: 360, + overflow: 'auto', + marginBottom: theme.spacing(2.75), + }, + section: { + margin: theme.spacing(4, 0), + '&:first-child': { + marginTop: 0, + }, + '&:last-child': { + marginBottom: 0, + }, + }, + placeholder: { + textAlign: 'center', + marginTop: 170, + }, + title: { + fontSize: 18, + fontWeight: 500, + lineHeight: '24px', + marginBottom: theme.spacing(4), + }, + content: { + fontSize: 14, + lineHeight: '24px', + }, +})) + +export interface DetailsTabProps { + boxInfo: BoxInfo + boxMetadata?: BoxMetadata +} + +export function DetailsTab(props: DetailsTabProps) { + const { boxInfo, boxMetadata } = props + const { classes } = useStyles() + + const definitions = boxMetadata?.activities.map((x) => ({ + title: x.title, + content: x.body, + })) + + if (!definitions) + return ( + + + No detailed information. + + + ) + + return ( + + {definitions?.map((x, i) => ( +
+ + {x.title} + + + {x.content} + +
+ ))} +
+ ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawDialog.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawDialog.tsx new file mode 100644 index 000000000000..edebf9dda67d --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawDialog.tsx @@ -0,0 +1,272 @@ +import { useCallback } from 'react' +import BigNumber from 'bignumber.js' +import { useContainer } from 'unstated-next' +import { makeStyles } from '@masknet/theme' +import { Add, Remove } from '@material-ui/icons' +import { FormattedAddress, FormattedBalance, ProviderIcon } from '@masknet/shared' +import { Box, Button, DialogContent, TextField, Typography } from '@material-ui/core' +import { + formatBalance, + useAccount, + useProviderType, + useChainId, + useMaskBoxConstants, + EthereumTokenType, +} from '@masknet/web3-shared' +import { InjectedDialog } from '../../../../components/shared/InjectedDialog' +import ActionButton from '../../../../extension/options-page/DashboardComponents/ActionButton' +import { EthereumERC20TokenApprovedBoundary } from '../../../../web3/UI/EthereumERC20TokenApprovedBoundary' +import { EthereumWalletConnectedBoundary } from '../../../../web3/UI/EthereumWalletConnectedBoundary' +import type { BoxInfo } from '../../type' +import { GasSettingBar } from '../../../Wallet/SNSAdaptor/GasSettingDialog/GasSettingBar' +import { TokenPrice } from '../../../../components/shared/TokenPrice' +import { Context } from '../../hooks/useContext' + +const useStyles = makeStyles()((theme) => ({ + main: { + padding: `${theme.spacing(2.5)} !important`, + }, + caption: { + textAlign: 'center', + }, + body: { + padding: theme.spacing(0, 2.5), + }, + value: { + fontSize: 32, + lineHeight: '40px', + fontWeight: 'bold', + marginBottom: theme.spacing(1), + marginRight: theme.spacing(1), + }, + section: { + padding: theme.spacing(2, 0), + justifyContent: 'space-between', + }, + title: { + width: '50%', + }, + content: {}, + field: { + borderRadius: 0, + padding: theme.spacing(0), + height: `25px !important`, + minWidth: 0, + minHeight: 0, + outline: `none !important`, + borderColor: `${theme.palette.divider} !important`, + }, + textfield: { + width: 40, + textAlign: 'center', + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: theme.palette.divider, + }, + '&:hover fieldset': { + borderColor: theme.palette.divider, + }, + '&.Mui-focused fieldset': { + borderWidth: 1, + borderColor: theme.palette.divider, + }, + }, + }, + number: { + '&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': { + appearance: 'none', + margin: 0, + }, + padding: 0, + appearance: 'textfield', + textAlign: 'center', + borderImage: 'none', + }, +})) + +export interface DrawDialogProps { + boxInfo: BoxInfo + open: boolean + onClose: () => void + onSubmit: () => Promise +} + +export function DrawDialog(props: DrawDialogProps) { + const { classes } = useStyles() + const { boxInfo, open, onClose, onSubmit } = props + + const { MASK_BOX_CONTRACT_ADDRESS } = useMaskBoxConstants() + + const { + contractDetailed, + paymentCount, + setPaymentCount, + paymentTokenPrice, + paymentTokenBalance, + paymentTokenDetailed, + + openBoxTransaction, + openBoxTransactionGasLimit, + openBoxTransactionOverrides, + setOpenBoxTransactionOverrides, + } = useContainer(Context) + + const account = useAccount() + const chainId = useChainId() + const providerType = useProviderType() + + const onCount = useCallback( + (step: number) => { + setPaymentCount(paymentCount + step) + }, + [paymentCount], + ) + + return ( + + + + + + + + + {paymentTokenDetailed?.symbol} + + {paymentTokenDetailed ? ( + + + + + ) : null} + + + + + Mystery Boxes: + + + + setPaymentCount(Number.parseInt(ev.target.value, 10))} + InputProps={{ + classes: { + root: classes.field, + }, + autoFocus: true, + inputProps: { + className: classes.number, + autoComplete: 'off', + autoCorrect: 'off', + title: 'Token Amount', + inputMode: 'decimal', + min: 0, + max: 999, + minLength: 1, + pattern: '^[0-9]*[.,]?[0-9]*$', + spellCheck: false, + }, + }} + /> + + + + + + Quantity Limit: + + + {boxInfo.personalLimit} + {boxInfo.tokenIdsPurchased.length ? ` (${boxInfo.personalRemaining} remaining)` : ''} + + + + + Current Wallet: + + + + + + + + + + + Available: + + + + {formatBalance(paymentTokenBalance, paymentTokenDetailed?.decimals ?? 0, 6)}{' '} + {paymentTokenDetailed?.symbol} + + + + + + Gas Fee: + + + + + + + + + + + + Draw + + + + + + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawResultDialog.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawResultDialog.tsx new file mode 100644 index 000000000000..36dd952f889e --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/DrawResultDialog.tsx @@ -0,0 +1,72 @@ +import { useCallback } from 'react' +import { difference } from 'lodash-es' +import { useContainer } from 'unstated-next' +import { makeStyles } from '@masknet/theme' +import { Box, DialogContent } from '@material-ui/core' +import type { ERC721ContractDetailed } from '@masknet/web3-shared' +import type { BoxInfo } from '../../type' +import { TokenCard } from './TokenCard' +import { InjectedDialog } from '../../../../components/shared/InjectedDialog' +import ActionButton from '../../../../extension/options-page/DashboardComponents/ActionButton' +import { activatedSocialNetworkUI } from '../../../../social-network' +import { usePostLink } from '../../../../components/DataSource/usePostInfo' +import { Context } from '../../hooks/useContext' + +const useStyles = makeStyles()((theme) => ({ + main: {}, + list: { + height: 360, + overflow: 'auto', + marginBottom: theme.spacing(2), + display: 'grid', + flexWrap: 'wrap', + gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', + gridGap: theme.spacing(1), + }, + token: {}, +})) + +export interface DrawResultDialogProps { + open: boolean + onClose: () => void + boxInfo: BoxInfo + contractDetailed?: ERC721ContractDetailed +} + +export function DrawResultDialog(props: DrawResultDialogProps) { + const { open, onClose, boxInfo, contractDetailed } = props + const { classes } = useStyles() + + const { lastPurchasedTokenIds, setLastPurchasedTokenIds } = useContainer(Context) + + const postLink = usePostLink() + const shareSuccessLink = activatedSocialNetworkUI.utils.getShareLinkURL?.( + `I just claimed a #MaskBox with @realMaskNetwork. Install mask.io and create your own NFT mystery box! \n ${postLink}`, + ) + + const onShare = useCallback(() => { + onClose() + window.open(shareSuccessLink, '_blank', 'noopener noreferrer') + }, [shareSuccessLink]) + + if (!contractDetailed) return null + + return ( + + + + + {difference(boxInfo.tokenIdsPurchased, lastPurchasedTokenIds).map((x) => ( + + + + ))} + + + Share + + + + + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/PreviewCard.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/PreviewCard.tsx new file mode 100644 index 000000000000..561536ee5a32 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/PreviewCard.tsx @@ -0,0 +1,219 @@ +import { useCallback, useEffect, useState } from 'react' +import { useContainer } from 'unstated-next' +import { makeStyles } from '@masknet/theme' +import { Box, Button, Skeleton, Typography } from '@material-ui/core' +import { formatBalance, TransactionStateType, useTransactionCallback } from '@masknet/web3-shared' +import AbstractTab, { AbstractTabProps } from '../../../../components/shared/AbstractTab' +import { EthereumWalletConnectedBoundary } from '../../../../web3/UI/EthereumWalletConnectedBoundary' +import ActionButton from '../../../../extension/options-page/DashboardComponents/ActionButton' +import { DrawDialog } from './DrawDialog' +import { Context } from '../../hooks/useContext' +import { BoxState, CardTab } from '../../type' +import { ArticlesTab } from './ArticlesTab' +import { DetailsTab } from './DetailsTab' +import { DrawResultDialog } from './DrawResultDialog' +import { useRemoteControlledDialog } from '@masknet/shared' +import { WalletMessages } from '../../../Wallet/messages' + +const useTabsStyles = makeStyles()((theme) => ({ + tab: { + height: 36, + minHeight: 36, + fontWeight: 300, + }, + tabs: { + width: '100%', + height: 36, + minHeight: 36, + margin: `0 ${location.host.includes('minds') ? '12px' : 'auto'}`, + '& .Mui-selected': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + }, + borderRadius: 4, + }, + indicator: { + display: 'none', + }, + tabPanel: { + marginTop: `${theme.spacing(2)} !important`, + }, +})) + +export interface PreviewCardProps {} + +export function PreviewCard(props: PreviewCardProps) { + const { classes: tabClasses } = useTabsStyles() + const state = useState(CardTab.Articles) + const [openDrawDialog, setOpenDrawDialog] = useState(false) + const [openDrawResultDialog, setOpenDrawResultDialog] = useState(false) + + const { + boxId, + boxState, + boxStateMessage, + boxInfo, + boxMetadata, + contractDetailed, + paymentCount, + setPaymentCount, + paymentTokenAddress, + setPaymentTokenAddress, + paymentTokenPrice, + paymentTokenDetailed, + + // token ids + lastPurchasedTokenIds, + refreshLastPurchasedTokenIds, + + // transaction + openBoxTransaction, + openBoxTransactionOverrides, + openBoxTransactionGasLimit, + setOpenBoxTransactionOverrides, + + // retry + retryMaskBoxInfo, + retryBoxInfo, + retryMaskBoxCreationSuccessEvent, + retryMaskBoxTokensForSale, + retryMaskBoxPurchasedTokens, + } = useContainer(Context) + + //#region open box + const [openBoxState, openBoxCallback, resetOpenBoxCallback] = useTransactionCallback( + TransactionStateType.CONFIRMED, + { + ...openBoxTransaction?.config, + gas: openBoxTransactionOverrides?.gas ?? openBoxTransactionGasLimit, + }, + openBoxTransaction?.method, + ) + const onRefresh = useCallback(() => { + state[1](CardTab.Articles) + setPaymentCount(1) + setPaymentTokenAddress('') + resetOpenBoxCallback() + retryMaskBoxInfo() + retryMaskBoxCreationSuccessEvent() + retryMaskBoxTokensForSale() + retryMaskBoxPurchasedTokens() + }, [ + resetOpenBoxCallback, + retryMaskBoxInfo, + retryMaskBoxCreationSuccessEvent, + retryMaskBoxTokensForSale, + retryMaskBoxPurchasedTokens, + ]) + const onDraw = useCallback(async () => { + setOpenDrawDialog(false) + refreshLastPurchasedTokenIds() + await openBoxCallback() + }, [openBoxCallback, refreshLastPurchasedTokenIds]) + + const { setDialog: setTransactionDialog } = useRemoteControlledDialog( + WalletMessages.events.transactionDialogUpdated, + (ev) => { + if (ev.open) return + const isConfirmed = openBoxState.type === TransactionStateType.CONFIRMED + if (isConfirmed) { + onRefresh() + setOpenDrawResultDialog(true) + } + }, + ) + + useEffect(() => { + if (openBoxState.type === TransactionStateType.UNKNOWN) return + setTransactionDialog({ + open: true, + state: openBoxState, + summary: `Open ${boxInfo?.name ?? 'box'}...`, + }) + }, [openBoxState.type]) + //#endregion + + if (boxState === BoxState.UNKNOWN) + return ( + + + + + ) + if (boxState === BoxState.ERROR) + return ( + + Something went wrong. + + + ) + if (boxState === BoxState.NOT_FOUND || !boxInfo) + return ( + + Failed to load box. + + + ) + + const tabProps: AbstractTabProps = { + tabs: [ + { + label: 'Articles', + children: boxInfo ? : null, + sx: { p: 0 }, + }, + { + label: 'Details', + children: boxInfo ? : null, + sx: { p: 0 }, + }, + ], + state, + classes: tabClasses, + } + + return ( + + + + setOpenDrawDialog(true)}> + {boxState === BoxState.READY && paymentTokenAddress ? ( + <> + {boxStateMessage} ({formatBalance(paymentTokenPrice, paymentTokenDetailed?.decimals ?? 0)}{' '} + {paymentTokenDetailed?.symbol} each box) + + ) : ( + boxStateMessage + )} + + + { + setOpenBoxTransactionOverrides(null) + setOpenDrawDialog(false) + }} + onSubmit={onDraw} + /> + { + refreshLastPurchasedTokenIds() + setOpenDrawResultDialog(false) + }} + /> + + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx new file mode 100644 index 000000000000..c2c8583c0a42 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/components/TokenCard.tsx @@ -0,0 +1,45 @@ +import { makeStyles } from '@masknet/theme' +import { Typography } from '@material-ui/core' +import { CollectibleProvider, ERC721ContractDetailed, useERC721TokenDetailed } from '@masknet/web3-shared' +import { CollectibleCard } from '../../../../extension/options-page/DashboardComponents/CollectibleList/CollectibleCard' + +const useStyles = makeStyles()((theme) => ({ + title: { + textAlign: 'center', + margin: theme.spacing(1, 0), + maxWidth: 160, + }, + name: { + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + }, +})) + +export interface TokenCardProps { + tokenId: string + contractDetailed: ERC721ContractDetailed +} + +export function TokenCard(props: TokenCardProps) { + const { contractDetailed, tokenId } = props + const { classes } = useStyles() + const { + value: tokenDetailed = { + tokenId, + contractDetailed, + info: {}, + }, + } = useERC721TokenDetailed(contractDetailed, tokenId) + + return ( + <> + +
+ + {tokenDetailed.info.name ?? tokenId} + +
+ + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/index.tsx b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/index.tsx new file mode 100644 index 000000000000..48064c65a64f --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/SNSAdaptor/index.tsx @@ -0,0 +1,53 @@ +import { Suspense, useMemo } from 'react' +import { Plugin, usePostInfoDetails } from '@masknet/plugin-infra' +import { SnackbarContent } from '@material-ui/core' +import { base } from '../base' +import { parseURL } from '../../../utils/utils' +import { extractTextFromTypedMessage } from '../../../protocols/typed-message' +import { EthereumChainBoundary } from '../../../web3/UI/EthereumChainBoundary' +import { PreviewCard } from './components/PreviewCard' +import MaskPluginWrapper from '../../MaskPluginWrapper' +import { Context } from '../hooks/useContext' + +const isMaskBox = (x: string) => x.startsWith('https://box-beta.mask.io') || x.startsWith('https://box.mask.io') + +const sns: Plugin.SNSAdaptor.Definition = { + ...base, + init(signal) {}, + DecryptedInspector(props) { + const text = useMemo(() => extractTextFromTypedMessage(props.message), [props.message]) + const link = useMemo(() => parseURL(text.val || ''), [text.val]).find(isMaskBox) + if (!text.ok) return null + if (!link) return null + return + }, + PostInspector: function Component() { + const link = usePostInfoDetails + .postMetadataMentionedLinks() + .concat(usePostInfoDetails.postMentionedLinks()) + .find(isMaskBox) + if (!link) return null + return + }, +} + +export default sns + +function Renderer(props: React.PropsWithChildren<{ url: string }>) { + const [, chainId] = props.url.match(/chain=(\d+)/i) ?? [] + const [, boxId] = props.url.match(/box=(\d+)/i) ?? [] + + if (!chainId || !boxId) return null + + return ( + + }> + + + + + + + + ) +} diff --git a/packages/maskbook/src/plugins/MaskBox/Worker/index.ts b/packages/maskbook/src/plugins/MaskBox/Worker/index.ts new file mode 100644 index 000000000000..824d930b57e1 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/Worker/index.ts @@ -0,0 +1,9 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { base } from '../base' +import '../messages' + +const worker: Plugin.Worker.Definition = { + ...base, + init(signal) {}, +} +export default worker diff --git a/packages/maskbook/src/plugins/MaskBox/apis/index.ts b/packages/maskbook/src/plugins/MaskBox/apis/index.ts new file mode 100644 index 000000000000..d6a8c7357c73 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/apis/index.ts @@ -0,0 +1 @@ +export * from './rss3' diff --git a/packages/maskbook/src/plugins/MaskBox/apis/rss3.ts b/packages/maskbook/src/plugins/MaskBox/apis/rss3.ts new file mode 100644 index 000000000000..2b3feee968d9 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/apis/rss3.ts @@ -0,0 +1,22 @@ +import RSS3 from 'rss3-next' +import { RSS3_ENDPOINT } from '../constants' +import { personalSign } from '../../../extension/background-script/EthereumService' +import type { BoxMetadata } from '../type' + +async function createRSS(address: string) { + return new RSS3({ + endpoint: RSS3_ENDPOINT, + address, + sign: async (message: string) => { + return personalSign(message, address) + }, + }) +} + +export async function getMaskBoxMetadata(boxId: string, creator: string) { + const rss = await createRSS(creator) + const file = await rss.files.get(rss.account.address) + if (!file) throw new Error('The account was not found.') + const boxes = Object.getOwnPropertyDescriptor(file, '_box')?.value as Record | undefined + return boxes?.[boxId] +} diff --git a/packages/maskbook/src/plugins/MaskBox/base.ts b/packages/maskbook/src/plugins/MaskBox/base.ts new file mode 100644 index 000000000000..8e48afc090d6 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/base.ts @@ -0,0 +1,18 @@ +import type { Plugin } from '@masknet/plugin-infra' +import { languages } from './locales' +import { PLUGIN_IDENTIFIER } from './constants' + +export const base: Plugin.Shared.Definition = { + ID: PLUGIN_IDENTIFIER, + icon: '🎁', + name: { fallback: 'MaskBox' }, + description: { fallback: 'The mystery box with NFT inside which is provided by Mask Network.' }, + publisher: { name: { fallback: 'Mask Network' }, link: 'https://mask.io/' }, + enableRequirement: { + architecture: { app: false, web: true }, + networks: { type: 'opt-out', networks: {} }, + target: 'stable', + }, + experimentalMark: true, + i18n: languages, +} diff --git a/packages/maskbook/src/plugins/MaskBox/constants.ts b/packages/maskbook/src/plugins/MaskBox/constants.ts new file mode 100644 index 000000000000..62a006d31aa5 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/constants.ts @@ -0,0 +1,3 @@ +export const PLUGIN_IDENTIFIER = 'com.maskbook.box' + +export const RSS3_ENDPOINT = 'https://hub.pass3.me' diff --git a/packages/maskbook/src/plugins/MaskBox/helpers/formatCountdown.ts b/packages/maskbook/src/plugins/MaskBox/helpers/formatCountdown.ts new file mode 100644 index 000000000000..5eccf880a8bb --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/helpers/formatCountdown.ts @@ -0,0 +1,32 @@ +const msPerSecond = 1000 +const msPerMinute = msPerSecond * 60 +const msPerHour = msPerMinute * 60 +const msPerDay = msPerHour * 24 + +const units = [ + ['day', 'days'], + ['hour', 'hours'], + ['minute', 'minutes'], + ['second', 'seconds'], +] + +export function formatCountdown(countdown: number) { + if (countdown <= 0) return '' + + const amounts: number[] = [] + + ;[msPerDay, msPerHour, msPerMinute, msPerSecond].reduce((accumulator, x) => { + amounts.push(Math.floor(accumulator / x)) + return accumulator % x + }, countdown) + + return amounts + .map((x, i) => { + if (x <= 0) return '' + if (x === 1) return `${x} ${units[i][0]}` + return `${x} ${units[i][1]}` + }) + .filter(Boolean) + .join(' ') + .trim() +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useContext.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useContext.ts new file mode 100644 index 000000000000..64b5c119528e --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useContext.ts @@ -0,0 +1,274 @@ +import { useEffect, useMemo, useState, useCallback } from 'react' +import { useAsyncRetry } from 'react-use' +import { omit, clamp, first, uniq } from 'lodash-es' +import BigNumber from 'bignumber.js' +import { createContainer } from 'unstated-next' +import { unreachable } from '@dimensiondev/kit' +import { + ChainId, + isSameAddress, + useERC20TokenBalance, + useNativeTokenBalance, + useTokenConstants, + useFungibleTokensDetailed, + EthereumTokenType, + useChainId, + useAccount, + useERC721ContractDetailed, + addGasMargin, +} from '@masknet/web3-shared' +import type { NonPayableTx } from '@masknet/web3-contracts/types/types' +import { BoxInfo, BoxState } from '../type' +import { useMaskBoxInfo } from './useMaskBoxInfo' +import { useMaskBoxCreationSuccessEvent } from './useMaskBoxCreationSuccessEvent' +import { useMaskBoxTokensForSale } from './useMaskBoxTokensForSale' +import { useMaskBoxPurchasedTokens } from './useMaskBoxPurchasedTokens' +import { useHeartBit } from './useHeartBit' +import { formatCountdown } from '../helpers/formatCountdown' +import { useOpenBoxTransaction } from './useOpenBoxTransaction' +import { useMaskBoxMetadata } from './useMaskBoxMetadata' + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +function useContext(initialState?: { boxId: string }) { + const heartBit = useHeartBit() + + const account = useAccount() + const chainId = useChainId() + const { NATIVE_TOKEN_ADDRESS } = useTokenConstants(ChainId.Mainnet) + + const [boxId, setBoxId] = useState(initialState?.boxId ?? '') + const [paymentTokenAddress, setPaymentTokenAddress] = useState('') + + //#region the box info + const { + value: maskBoxInfo = null, + error: errorMaskBoxInfo, + loading: loadingMaskBoxInfo, + retry: retryMaskBoxInfo, + } = useMaskBoxInfo(boxId) + const { value: maskBoxCreationSuccessEvent = null, retry: retryMaskBoxCreationSuccessEvent } = + useMaskBoxCreationSuccessEvent(maskBoxInfo?.creator ?? '', maskBoxInfo?.nft_address ?? '', boxId) + const { value: paymentTokens = [] } = useFungibleTokensDetailed( + maskBoxInfo?.payment?.map(([address]) => { + return { + type: isSameAddress(address, ZERO_ADDRESS) ? EthereumTokenType.Native : EthereumTokenType.ERC20, + address, + } + }) ?? [], + chainId, + ) + const { value: allTokens = [], retry: retryMaskBoxTokensForSale } = useMaskBoxTokensForSale(boxId) + const { value: purchasedTokens = [], retry: retryMaskBoxPurchasedTokens } = useMaskBoxPurchasedTokens( + boxId, + account, + ) + + const { + value: boxInfo = null, + error: errorBoxInfo, + loading: loadingBoxInfo, + retry: retryBoxInfo, + } = useAsyncRetry(async () => { + if ( + !maskBoxInfo || + isSameAddress(maskBoxInfo?.creator ?? ZERO_ADDRESS, ZERO_ADDRESS) || + !maskBoxCreationSuccessEvent + ) + return null + const personalLimit = Number.parseInt(maskBoxInfo.personal_limit, 10) + const info: BoxInfo = { + boxId, + creator: maskBoxInfo.creator, + name: maskBoxInfo.name, + sellAll: maskBoxCreationSuccessEvent.returnValues.sell_all, + personalLimit: personalLimit, + personalRemaining: Math.max(0, personalLimit - purchasedTokens.length), + remaining: Number.parseInt(maskBoxInfo.remaining, 10), + startAt: new Date(Number.parseInt(maskBoxCreationSuccessEvent.returnValues.start_time, 10) * 1000), + endAt: new Date(Number.parseInt(maskBoxCreationSuccessEvent.returnValues.end_time, 10) * 1000), + total: maskBoxInfo.total, + tokenIds: allTokens, + tokenIdsPurchased: purchasedTokens, + payments: paymentTokens.map((token, i) => { + return { + token: token, + price: maskBoxInfo.payment[i][1], + receivableAmount: maskBoxInfo.payment[i][2], + } + }), + tokenAddress: maskBoxInfo.nft_address, + heroImageURL: '', + qualificationAddress: maskBoxInfo.qualification, + } + return Promise.resolve(info) + }, [ + allTokens.join(), + purchasedTokens.join(), + paymentTokens?.map((x) => x.address).join(), + maskBoxInfo, + maskBoxCreationSuccessEvent, + ]) + + const boxState = useMemo(() => { + if (errorMaskBoxInfo || errorBoxInfo) return BoxState.ERROR + if (loadingMaskBoxInfo || loadingBoxInfo) return BoxState.UNKNOWN + if (maskBoxInfo && !boxInfo) return BoxState.UNKNOWN + if (!maskBoxInfo || !boxInfo) return BoxState.NOT_FOUND + const now = new Date() + if (new BigNumber(boxInfo.tokenIdsPurchased.length).isGreaterThanOrEqualTo(boxInfo.personalLimit)) + return BoxState.DRAWED_OUT + if (new BigNumber(boxInfo.remaining).isLessThanOrEqualTo(0)) return BoxState.SOLD_OUT + if (boxInfo.startAt > now) return BoxState.NOT_READY + if (boxInfo.endAt < now || maskBoxInfo?.expired) return BoxState.EXPIRED + return BoxState.READY + }, [boxInfo, loadingBoxInfo, errorBoxInfo, maskBoxInfo, loadingMaskBoxInfo, errorMaskBoxInfo, heartBit]) + + const boxStateMessage = useMemo(() => { + switch (boxState) { + case BoxState.UNKNOWN: + return 'Loading...' + case BoxState.READY: + return 'Draw' + case BoxState.EXPIRED: + return 'Ended' + case BoxState.NOT_READY: + const now = Date.now() + const startAt = boxInfo?.startAt.getTime() ?? 0 + if (startAt <= now) return 'Loading...' + const countdown = formatCountdown(startAt - now) + return countdown ? `Start sale in ${countdown}` : 'Loading...' + case BoxState.SOLD_OUT: + return 'Sold Out' + case BoxState.DRAWED_OUT: + return 'Drawed Out' + case BoxState.ERROR: + return 'Something went wrong.' + case BoxState.NOT_FOUND: + return 'Failed to load box info.' + default: + unreachable(boxState) + } + }, [boxState, heartBit]) + //#endregion + + //#region the box metadata + const { value: boxMetadata, retry: retryBoxMetadata } = useMaskBoxMetadata(boxId, boxInfo?.creator ?? '') + //#endregion + + //#region the erc721 contract detailed + const { value: contractDetailed } = useERC721ContractDetailed(maskBoxInfo?.nft_address) + //#endregion + + //#region the payment count + const [paymentCount, setPaymentCount] = useState(1) + const setPaymentCount_ = useCallback( + (count: number) => { + setPaymentCount(clamp(count || 1, 1, boxInfo?.personalRemaining ?? 1)) + }, + [boxInfo?.personalRemaining], + ) + //#endregion + + //#region token ids + const [lastAllTokenIds, setLastAllTokenIds] = useState([]) + const [lastPurchasedTokenIds, setLastPurchasedTokenIds] = useState([]) + const refreshLastPurchasedTokenIds = useCallback(() => { + setLastPurchasedTokenIds((tokenIds) => uniq([...tokenIds, ...purchasedTokens])) + }, [purchasedTokens.length]) + //#endregion + + //#region the payment token + const { value: paymentNativeTokenBalance = '0' } = useNativeTokenBalance() + const { value: paymentERC20TokenBalance = '0' } = useERC20TokenBalance( + isSameAddress(paymentTokenAddress, NATIVE_TOKEN_ADDRESS) ? '' : paymentTokenAddress, + ) + const paymentTokenInfo = boxInfo?.payments.find((x) => isSameAddress(x.token.address, paymentTokenAddress)) + const paymentTokenIndex = + boxInfo?.payments.findIndex((x) => isSameAddress(x.token.address ?? '', paymentTokenAddress)) ?? -1 + const paymentTokenPrice = paymentTokenInfo?.price ?? '0' + const paymentTokenBalance = isSameAddress(paymentTokenAddress, NATIVE_TOKEN_ADDRESS) + ? paymentNativeTokenBalance + : paymentERC20TokenBalance + const paymentTokenDetailed = paymentTokenInfo?.token ?? null + + useEffect(() => { + const firstPaymentTokenAddress = first(boxInfo?.payments)?.token.address + if (paymentTokenAddress === '' && firstPaymentTokenAddress) setPaymentTokenAddress(firstPaymentTokenAddress) + }, [paymentTokenAddress, boxInfo?.payments.map((x) => x.token.address).join()]) + //#endregion + + //#region transactions + const [openBoxTransactionOverrides, setOpenBoxTransactionOverrides] = useState(null) + const openBoxTransaction = useOpenBoxTransaction( + boxId, + paymentCount, + paymentTokenIndex, + paymentTokenPrice, + paymentTokenDetailed, + openBoxTransactionOverrides, + ) + const { value: openBoxTransactionGasLimit = 0 } = useAsyncRetry(async () => { + if (!openBoxTransaction) return 0 + const estimatedGas = await openBoxTransaction.method.estimateGas(omit(openBoxTransaction.config, 'gas')) + return addGasMargin(estimatedGas).toNumber() + }, [openBoxTransaction]) + //#endregion + + return { + // box id + boxId, + setBoxId, + + // box info & metadata + boxInfo, + boxMetadata, + + // box state + boxState, + boxStateMessage, + + // erc721 contract detailed + contractDetailed, + + // payment count + paymentCount, + setPaymentCount: setPaymentCount_, + + // payment address + paymentTokenAddress: paymentTokenAddress || (first(boxInfo?.payments)?.token.address ?? ''), + setPaymentTokenAddress: (address: string) => { + if (boxInfo?.payments.some((x) => isSameAddress(x.token.address ?? '', address))) + setPaymentTokenAddress(address) + }, + + // token ids + lastAllTokenIds, + setLastAllTokenIds, + lastPurchasedTokenIds, + setLastPurchasedTokenIds, + refreshLastPurchasedTokenIds, + + // payment token + paymentTokenPrice, + paymentTokenIndex, + paymentTokenBalance, + paymentTokenDetailed, + + // transactions + openBoxTransaction, + openBoxTransactionGasLimit, + openBoxTransactionOverrides, + setOpenBoxTransactionOverrides, + + // retry callbacks + retryMaskBoxInfo, + retryBoxInfo, + retryBoxMetadata, + retryMaskBoxCreationSuccessEvent, + retryMaskBoxTokensForSale, + retryMaskBoxPurchasedTokens, + } +} + +export const Context = createContainer(useContext) diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useHeartBit.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useHeartBit.ts new file mode 100644 index 000000000000..13d5662e5b61 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useHeartBit.ts @@ -0,0 +1,11 @@ +import { useState } from 'react' +import { useTimeoutFn } from 'react-use' + +export function useHeartBit(delay = 1000) { + const [bit, setBit] = useState(0) + const [, , reset] = useTimeoutFn(() => { + setBit((x) => (x + 1) % Number.MAX_SAFE_INTEGER) + reset() + }, delay) + return bit +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxContract.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxContract.ts new file mode 100644 index 000000000000..718b1f59b1ff --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxContract.ts @@ -0,0 +1,9 @@ +import type { AbiItem } from 'web3-utils' +import type { MaskBox } from '@masknet/web3-contracts/types/MaskBox' +import MASK_BOX_ABI from '@masknet/web3-contracts/abis/MaskBox.json' +import { useContract, useMaskBoxConstants } from '@masknet/web3-shared' + +export function useMaskBoxContract() { + const { MASK_BOX_CONTRACT_ADDRESS } = useMaskBoxConstants() + return useContract(MASK_BOX_CONTRACT_ADDRESS, MASK_BOX_ABI as AbiItem[]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxCreationSuccessEvent.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxCreationSuccessEvent.ts new file mode 100644 index 000000000000..97e47df27054 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxCreationSuccessEvent.ts @@ -0,0 +1,23 @@ +import { first } from 'lodash-es' +import { useAsyncRetry } from 'react-use' +import type { CreationSuccess } from '@masknet/web3-contracts/types/MaskBox' +import { useMaskBoxConstants } from '@masknet/web3-shared' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useMaskBoxCreationSuccessEvent(creatorAddress: string, tokenAddress: string, boxId: string) { + const maskBoxContract = useMaskBoxContract() + const { MASK_BOX_CONTRACT_FROM_BLOCK } = useMaskBoxConstants() + + return useAsyncRetry(async () => { + if (!maskBoxContract) return null + const events = await maskBoxContract.getPastEvents('CreationSuccess', { + filter: { + creator: creatorAddress, + nft_address: tokenAddress, + box_id: boxId, + }, + fromBlock: MASK_BOX_CONTRACT_FROM_BLOCK, + }) + return first(events as unknown as CreationSuccess[]) + }, [boxId, creatorAddress, tokenAddress, maskBoxContract, MASK_BOX_CONTRACT_FROM_BLOCK]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxInfo.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxInfo.ts new file mode 100644 index 000000000000..37e0bfaa3b7e --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxInfo.ts @@ -0,0 +1,10 @@ +import { useAsyncRetry } from 'react-use' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useMaskBoxInfo(id: string | number) { + const maskBoxContract = useMaskBoxContract() + return useAsyncRetry(async () => { + if (!maskBoxContract) return null + return maskBoxContract.methods.getBoxInfo(id).call() + }, [id, maskBoxContract]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxMetadata.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxMetadata.ts new file mode 100644 index 000000000000..01cce7c99f88 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxMetadata.ts @@ -0,0 +1,10 @@ +import { useAsyncRetry } from 'react-use' +import { EthereumAddress } from 'wallet.ts' +import { MaskBoxRPC } from '../messages' + +export function useMaskBoxMetadata(boxId: string, creator: string) { + return useAsyncRetry(async () => { + if (!boxId || !creator || !EthereumAddress.isValid(creator)) return + return MaskBoxRPC.getMaskBoxMetadata(boxId, creator) + }, [creator]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxPurchasedTokens.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxPurchasedTokens.ts new file mode 100644 index 000000000000..3602531946cc --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxPurchasedTokens.ts @@ -0,0 +1,10 @@ +import { useAsyncRetry } from 'react-use' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useMaskBoxPurchasedTokens(id: string | number, customer: string) { + const maskBoxContract = useMaskBoxContract() + return useAsyncRetry(async () => { + if (!maskBoxContract) return [] + return maskBoxContract.methods.getPurchasedNft(id, customer).call() + }, [id, customer, maskBoxContract]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxTokensForSale.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxTokensForSale.ts new file mode 100644 index 000000000000..34917c0058b9 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useMaskBoxTokensForSale.ts @@ -0,0 +1,10 @@ +import { useAsyncRetry } from 'react-use' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useMaskBoxTokensForSale(id: string | number) { + const maskBoxContract = useMaskBoxContract() + return useAsyncRetry(async () => { + if (!maskBoxContract) return [] + return maskBoxContract.methods.getNftListForSale(id, 0, 100).call() + }, [id, maskBoxContract]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxGasLimit.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxGasLimit.ts new file mode 100644 index 000000000000..3028608f0f2c --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxGasLimit.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react' +import type { PayableTx } from '@masknet/web3-contracts/types/types' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useOpenBoxGasLimit(parameters: [string, string, string, string], config: PayableTx) { + const maskBoxContract = useMaskBoxContract() + + return useMemo(() => { + if (!maskBoxContract) return '0' + return maskBoxContract.methods.openBox(...parameters).estimateGas(config) + }, [parameters, maskBoxContract]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxTransaction.ts b/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxTransaction.ts new file mode 100644 index 000000000000..69ec332e5b4a --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/hooks/useOpenBoxTransaction.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react' +import BigNumber from 'bignumber.js' +import type { NonPayableTx } from '@masknet/web3-contracts/types/types' +import { EthereumTokenType, FungibleTokenDetailed, useAccount } from '@masknet/web3-shared' +import { useMaskBoxContract } from './useMaskBoxContract' + +export function useOpenBoxTransaction( + boxId: string, + amount: number, + paymentTokenIndex: number, + paymentTokenPrice: string, + paymentTokenDetailed: FungibleTokenDetailed | null, + overrides?: NonPayableTx | null, +) { + const account = useAccount() + const maskBoxContract = useMaskBoxContract() + return useMemo(() => { + if (!boxId || amount <= 0 || !maskBoxContract) return + return { + config: { + ...overrides, + from: account, + value: + paymentTokenDetailed?.type === EthereumTokenType.Native + ? new BigNumber(paymentTokenPrice).multipliedBy(amount).toFixed() + : undefined, + }, + method: maskBoxContract.methods.openBox(boxId, amount, paymentTokenIndex, '0x0'), + } + }, [account, amount, boxId, maskBoxContract, paymentTokenIndex, paymentTokenPrice, paymentTokenDetailed, overrides]) +} diff --git a/packages/maskbook/src/plugins/MaskBox/index.ts b/packages/maskbook/src/plugins/MaskBox/index.ts new file mode 100644 index 000000000000..eb51ba0afe25 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/index.ts @@ -0,0 +1,16 @@ +import { registerPlugin } from '@masknet/plugin-infra' +import { base } from './base' + +registerPlugin({ + ...base, + SNSAdaptor: { + load: () => import('./SNSAdaptor'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./SNSAdaptor', () => hot(import('./SNSAdaptor'))), + }, + Worker: { + load: () => import('./Worker'), + hotModuleReload: (hot) => + import.meta.webpackHot && import.meta.webpackHot.accept('./Worker', () => hot(import('./Worker'))), + }, +}) diff --git a/packages/maskbook/src/plugins/MaskBox/locales/en-US.json b/packages/maskbook/src/plugins/MaskBox/locales/en-US.json new file mode 100644 index 000000000000..6585c4ff2c25 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/locales/en-US.json @@ -0,0 +1,4 @@ +{ + "name": "Example Plugin", + "__entry__": "🤔 Example" +} diff --git a/packages/maskbook/src/plugins/MaskBox/locales/index.ts b/packages/maskbook/src/plugins/MaskBox/locales/index.ts new file mode 100644 index 000000000000..36ffc53185b1 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/locales/index.ts @@ -0,0 +1,13 @@ +// This file is auto generated. DO NOT EDIT +// Run `npx gulp sync-languages` to regenerate. +// Default fallback language in a family of languages are chosen by the alphabet order +// To overwrite this, please overwrite packages/scripts/src/locale-kit-next/index.ts +import en_US from './en-US.json' +import zh_TW from './zh-TW.json' + +// export * from './i18n_generated' + +export const languages = { + en: en_US, + zh: zh_TW, +} diff --git a/packages/maskbook/src/plugins/MaskBox/locales/zh-TW.json b/packages/maskbook/src/plugins/MaskBox/locales/zh-TW.json new file mode 100644 index 000000000000..0230173f422c --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/locales/zh-TW.json @@ -0,0 +1,4 @@ +{ + "name": "測試插件", + "__entry__": "🤔 測試" +} diff --git a/packages/maskbook/src/plugins/MaskBox/messages.ts b/packages/maskbook/src/plugins/MaskBox/messages.ts new file mode 100644 index 000000000000..442bb6baeb45 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/messages.ts @@ -0,0 +1,9 @@ +import { createPluginMessage, createPluginRPC, PluginMessageEmitter } from '@masknet/plugin-infra' +import { PLUGIN_IDENTIFIER } from './constants' + +export interface MaskBoxMessages { + rpc: unknown +} + +export const MaskBoxMessages: PluginMessageEmitter = createPluginMessage(PLUGIN_IDENTIFIER) +export const MaskBoxRPC = createPluginRPC(PLUGIN_IDENTIFIER, () => import('./services'), MaskBoxMessages.rpc) diff --git a/packages/maskbook/src/plugins/MaskBox/services.ts b/packages/maskbook/src/plugins/MaskBox/services.ts new file mode 100644 index 000000000000..5914b704d67f --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/services.ts @@ -0,0 +1 @@ +export * from './apis' diff --git a/packages/maskbook/src/plugins/MaskBox/type.ts b/packages/maskbook/src/plugins/MaskBox/type.ts new file mode 100644 index 000000000000..871f3c76dc78 --- /dev/null +++ b/packages/maskbook/src/plugins/MaskBox/type.ts @@ -0,0 +1,61 @@ +import type { FungibleTokenDetailed } from '@masknet/web3-shared' + +export enum CardTab { + Articles = 0, + Details = 1, +} + +export enum BoxState { + UNKNOWN = 0, + NOT_READY = 1, + READY = 2, + EXPIRED = 3, + /** sold all tokens out */ + SOLD_OUT = 4, + /** drawed all personal limited tokens */ + DRAWED_OUT = 5, + /** error occur */ + ERROR = 6, + /** 404 */ + NOT_FOUND = 7, +} + +export interface PaymentOption { + tokenAddress: string + price: string +} + +export interface PaymentInfo { + token: FungibleTokenDetailed + price: string + receivableAmount: string +} + +export interface BoxInfo { + boxId: string + creator: string + name: string + sellAll: boolean + personalLimit: number + personalRemaining: number + payments: PaymentInfo[] + remaining: number + total: string + startAt: Date + endAt: Date + tokenIds: string[] + tokenIdsPurchased: string[] + tokenAddress: string + heroImageURL: string + qualificationAddress: string +} + +export interface BoxMetadata { + id: string + name: string + cover: string + activities: { + title: string + body: string + }[] +} diff --git a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx index aa814bef3fc2..bab3429cd446 100644 --- a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx +++ b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketDialog.tsx @@ -252,7 +252,6 @@ export default function RedPacketDialog(props: RedPacketDialogProps) { ], state, classes: { - // focusTab: classes.focusTab, tabPaper: classes.tabPaper, }, } diff --git a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx index a91823ba828e..6fa902f882e2 100644 --- a/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx +++ b/packages/maskbook/src/plugins/RedPacket/SNSAdaptor/RedPacketERC721Form.tsx @@ -236,7 +236,7 @@ export function RedPacketERC721Form(props: RedPacketERC721FormProps) { void +} + +export function GasSettingBar(props: GasSettingBarProps) { + const { gasLimit, gasPrice, maxFee, priorityFee, onChange } = props + + const chainId = useChainId() + const { value: nativeTokenDetailed } = useNativeTokenDetailed() + const { value: gasPriceDefault = '0' } = useGasPrice() + + const [gasOption, setGasOption] = useState(GasOption.Medium) + const { setDialog: setGasSettingDialog } = useRemoteControlledDialog(WalletMessages.events.gasSettingDialogUpdated) + const onOpenGasSettingDialog = useCallback(() => { + setGasSettingDialog( + isEIP1559Supported(chainId) + ? { + open: true, + gasLimit, + maxFee, + priorityFee, + gasOption, + } + : { + open: true, + gasLimit, + gasPrice, + gasOption, + }, + ) + }, [chainId, gasLimit, gasPrice, maxFee, priorityFee, gasOption]) + + // set initial options + useEffect(() => { + return WalletMessages.events.gasSettingDialogUpdated.on((evt) => { + if (evt.open) return + if (evt.gasOption) setGasOption(evt.gasOption) + onChange( + (isEIP1559Supported(chainId) + ? { + gas: evt.gasLimit, + maxFee: evt.maxFee, + priorityFee: evt.priorityFee, + } + : { + gas: evt.gasLimit, + gasPrice: evt.gasPrice, + }) as NonPayableTx, + ) + }) + }, []) + + const gasFee = useMemo(() => { + return new BigNumber(gasLimit).multipliedBy( + isEIP1559Supported(chainId) && maxFee ? new BigNumber(maxFee) : gasPrice ?? gasPriceDefault, + ) + }, [chainId, gasLimit, gasPrice, maxFee]) + + return ( + + + + {formatWeiToEther(gasFee).toFixed(6)} {nativeTokenDetailed?.symbol ?? ''} ≈ + + + + + + + + ) +} diff --git a/packages/maskbook/src/web3/UI/EthereumERC20TokenApprovedBoundary.tsx b/packages/maskbook/src/web3/UI/EthereumERC20TokenApprovedBoundary.tsx index 05f26ce220a4..bd452294d094 100644 --- a/packages/maskbook/src/web3/UI/EthereumERC20TokenApprovedBoundary.tsx +++ b/packages/maskbook/src/web3/UI/EthereumERC20TokenApprovedBoundary.tsx @@ -11,7 +11,7 @@ import { } from '@masknet/web3-shared' import { unreachable } from '@dimensiondev/kit' import { useI18N } from '../../utils' -import ActionButton from '../../extension/options-page/DashboardComponents/ActionButton' +import ActionButton, { ActionButtonProps } from '../../extension/options-page/DashboardComponents/ActionButton' const useStyles = makeStyles()((theme) => ({ button: { @@ -39,6 +39,7 @@ export interface EthereumERC20TokenApprovedBoundaryProps { token?: ERC20TokenDetailed fallback?: React.ReactNode children?: React.ReactNode | ((allowance: string) => React.ReactNode) + ActionButtonProps?: ActionButtonProps } export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenApprovedBoundaryProps) { @@ -70,7 +71,13 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr if (approveStateType === ApproveStateType.UNKNOWN) return ( - + {fallback ?? 'Enter an amount'} @@ -83,7 +90,8 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr fullWidth variant="contained" size="large" - onClick={resetApproveCallback}> + onClick={resetApproveCallback} + {...props.ActionButtonProps}> Failed to load {token.symbol ?? token.name ?? 'Token'}. Click to retry. @@ -97,7 +105,8 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr fullWidth variant="contained" size="large" - onClick={() => onApprove(true)}> + onClick={() => onApprove(true)} + {...props.ActionButtonProps}> {t('plugin_wallet_token_unlock')} {`${formatBalance(amount, token.decimals, 2)} ${ token?.symbol ?? 'Token' @@ -110,7 +119,8 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr fullWidth variant="contained" size="large" - onClick={() => onApprove(false)}> + onClick={() => onApprove(false)} + {...props.ActionButtonProps}> {t('plugin_wallet_token_infinite_unlock')} @@ -119,7 +129,13 @@ export function EthereumERC20TokenApprovedBoundary(props: EthereumERC20TokenAppr if (approveStateType === ApproveStateType.PENDING || approveStateType === ApproveStateType.UPDATING) return ( - + {approveStateType === ApproveStateType.PENDING ? t('plugin_ito_unlocking_symbol', { symbol: token.symbol }) : `Updating ${token.symbol}`} diff --git a/packages/maskbook/src/web3/UI/EthereumERC721TokenApprovedBoundary.tsx b/packages/maskbook/src/web3/UI/EthereumERC721TokenApprovedBoundary.tsx index 3d6161dce490..32d8b0b55b09 100644 --- a/packages/maskbook/src/web3/UI/EthereumERC721TokenApprovedBoundary.tsx +++ b/packages/maskbook/src/web3/UI/EthereumERC721TokenApprovedBoundary.tsx @@ -9,7 +9,7 @@ import OpenInNewIcon from '@material-ui/icons/OpenInNew' import { useI18N } from '../../utils' import { makeStyles, useSnackbar } from '@masknet/theme' import { Typography, Link } from '@material-ui/core' -import ActionButton from '../../extension/options-page/DashboardComponents/ActionButton' +import ActionButton, { ActionButtonProps } from '../../extension/options-page/DashboardComponents/ActionButton' import { useMemo, useEffect } from 'react' import { EthereumAddress } from 'wallet.ts' import { useStylesExtends } from '@masknet/shared' @@ -38,18 +38,19 @@ const useStyles = makeStyles()(() => ({ export interface EthereumERC712TokenApprovedBoundaryProps extends withClasses<'approveButton'> { children?: React.ReactNode owner: string | undefined - contract: ERC721ContractDetailed | undefined + contractDetailed: ERC721ContractDetailed | undefined validationMessage?: string operator: string | undefined + ActionButtonProps?: ActionButtonProps } export function EthereumERC721TokenApprovedBoundary(props: EthereumERC712TokenApprovedBoundaryProps) { - const { owner, contract, operator, children, validationMessage: _validationMessage } = props + const { owner, contractDetailed, operator, children, validationMessage: _validationMessage } = props const { t } = useI18N() const classes = useStylesExtends(useStyles(), props) - const { value, loading, retry } = useERC721ContractIsApproveForAll(contract?.address, owner, operator) + const { value, loading, retry } = useERC721ContractIsApproveForAll(contractDetailed?.address, owner, operator) const [approveState, approveCallback, resetCallback] = useERC721ContractSetApproveForAllCallback( - contract?.address, + contractDetailed?.address, operator, true, ) @@ -60,10 +61,13 @@ export function EthereumERC721TokenApprovedBoundary(props: EthereumERC712TokenAp enqueueSnackbar(
- {t('plugin_wallet_approve_all_nft_successfully', { symbol: contract?.symbol })} + {t('plugin_wallet_approve_all_nft_successfully', { symbol: contractDetailed?.symbol })} @@ -83,54 +87,75 @@ export function EthereumERC721TokenApprovedBoundary(props: EthereumERC712TokenAp }) resetCallback() } - }, [approveState, contract]) + }, [approveState, contractDetailed]) const validationMessage = useMemo(() => { - if (!contract?.address || !EthereumAddress.isValid(contract?.address)) + if (!contractDetailed?.address || !EthereumAddress.isValid(contractDetailed?.address)) return t('plugin_wallet_select_a_nft_contract') if (!owner || !EthereumAddress.isValid(owner)) return t('plugin_wallet_select_a_nft_owner') if (!operator || !EthereumAddress.isValid(operator)) return t('plugin_wallet_select_a_nft_operator') if (!!_validationMessage) return _validationMessage return '' - }, [contract, owner, operator, _validationMessage]) + }, [contractDetailed, owner, operator, _validationMessage]) if ([TransactionStateType.WAIT_FOR_CONFIRMING, TransactionStateType.HASH].includes(approveState.type)) { return ( - - {t('plugin_wallet_nft_approving_all', { symbol: contract?.symbol })} + + {t('plugin_wallet_nft_approving_all', { symbol: contractDetailed?.symbol })} ) } else if (!!validationMessage) { return ( - + {validationMessage} ) } else if (loading) { return ( ) } else if (value === false) { return ( - {t('plugin_wallet_approve_all_nft', { symbol: contract?.symbol })} + onClick={approveCallback} + {...props.ActionButtonProps}> + {t('plugin_wallet_approve_all_nft', { symbol: contractDetailed?.symbol })} ) } else if (value === undefined) { return ( - + {t('plugin_wallet_fail_to_load_nft_contract')} ) diff --git a/packages/maskbook/src/web3/UI/EthereumWalletConnectedBoundary.tsx b/packages/maskbook/src/web3/UI/EthereumWalletConnectedBoundary.tsx index 4cebe744e07f..a951fb78035f 100644 --- a/packages/maskbook/src/web3/UI/EthereumWalletConnectedBoundary.tsx +++ b/packages/maskbook/src/web3/UI/EthereumWalletConnectedBoundary.tsx @@ -2,7 +2,7 @@ import { Grid } from '@material-ui/core' import { makeStyles } from '@masknet/theme' import classNames from 'classnames' import { useRemoteControlledDialog, useStylesExtends } from '@masknet/shared' -import ActionButton from '../../extension/options-page/DashboardComponents/ActionButton' +import ActionButton, { ActionButtonProps } from '../../extension/options-page/DashboardComponents/ActionButton' import { WalletMessages } from '../../plugins/Wallet/messages' import { useI18N } from '../../utils' import { isZero, useAccount, useChainIdValid, useNativeTokenBalance } from '@masknet/web3-shared' @@ -19,6 +19,7 @@ export interface EthereumWalletConnectedBoundaryProps offChain?: boolean children?: React.ReactNode hideRiskWarningConfirmed?: boolean + ActionButtonProps?: ActionButtonProps } export function EthereumWalletConnectedBoundary(props: EthereumWalletConnectedBoundaryProps) { @@ -49,7 +50,8 @@ export function EthereumWalletConnectedBoundary(props: EthereumWalletConnectedBo fullWidth variant="contained" size="large" - onClick={openSelectProviderDialog}> + onClick={openSelectProviderDialog} + {...props.ActionButtonProps}> {t('plugin_wallet_connect_a_wallet')} @@ -59,11 +61,12 @@ export function EthereumWalletConnectedBoundary(props: EthereumWalletConnectedBo return ( + onClick={openRiskWarningDialog} + {...props.ActionButtonProps}> {t('plugin_wallet_confirm_risk_warning')} @@ -78,7 +81,8 @@ export function EthereumWalletConnectedBoundary(props: EthereumWalletConnectedBo fullWidth variant="contained" size="large" - onClick={nativeTokenBalance.retry}> + onClick={nativeTokenBalance.retry} + {...props.ActionButtonProps}> {t(nativeTokenBalance.loading ? 'plugin_wallet_update_gas_fee' : 'plugin_wallet_no_gas_fee')} @@ -92,7 +96,8 @@ export function EthereumWalletConnectedBoundary(props: EthereumWalletConnectedBo disabled fullWidth variant="contained" - size="large"> + size="large" + {...props.ActionButtonProps}> {t('plugin_wallet_invalid_network')} diff --git a/packages/web3-constants/data/mask-box.json b/packages/web3-constants/data/mask-box.json new file mode 100644 index 000000000000..c8084a4fc5ae --- /dev/null +++ b/packages/web3-constants/data/mask-box.json @@ -0,0 +1,72 @@ +{ + "MASK_BOX_CONTRACT_ADDRESS": { + "Mainnet": "0x0dFB34D213f613Dda67a2924F60b5301d42ABFb7", + "Ropsten": "", + "Rinkeby": "0xbFcf8210F5B6764D86a9C5252218ad627A6a949d", + "Kovan": "", + "Gorli": "", + "BSC": "", + "BSCT": "", + "Matic": "", + "Mumbai": "", + "Arbitrum": "", + "Arbitrum_Rinkeby": "", + "xDai": "" + }, + "MASK_BOX_CONTRACT_FROM_BLOCK": { + "Mainnet": 13396393, + "Ropsten": 0, + "Rinkeby": 9369286, + "Kovan": 0, + "Gorli": 0, + "BSC": 0, + "BSCT": 0, + "Matic": 0, + "Mumbai": 0, + "Arbitrum": 0, + "Arbitrum_Rinkeby": 0, + "xDai": 0 + }, + "MASK_BOX_TEST_NFT": { + "Mainnet": "", + "Ropsten": "", + "Rinkeby": "0x0c8FB5C985E00fb1D002b6B9700084492Fb4B9A8", + "Kovan": "", + "Gorli": "", + "BSC": "", + "BSCT": "", + "Matic": "", + "Mumbai": "", + "Arbitrum": "", + "Arbitrum_Rinkeby": "", + "xDai": "" + }, + "WHITE_LIST_QUALIFICATION_CONTRACT_ADDRESS": { + "Mainnet": "", + "Ropsten": "", + "Rinkeby": "0x996A9DCe6247cd8AaFA60de34cDD5332d9AdE702", + "Kovan": "", + "Gorli": "", + "BSC": "", + "BSCT": "", + "Matic": "", + "Mumbai": "", + "Arbitrum": "", + "Arbitrum_Rinkeby": "", + "xDai": "" + }, + "SIG_VERIFY_QULIFICATION_CONTRACT_ADDRESS": { + "Mainnet": "", + "Ropsten": "", + "Rinkeby": "0x9a656528700493348132823C6A3C59CdFa48283d", + "Kovan": "", + "Gorli": "", + "BSC": "", + "BSCT": "", + "Matic": "", + "Mumbai": "", + "Arbitrum": "", + "Arbitrum_Rinkeby": "", + "xDai": "" + } +} diff --git a/packages/web3-constants/data/nft-red-packet.json b/packages/web3-constants/data/nft-red-packet.json index 9defdb79ca29..e5861953638b 100644 --- a/packages/web3-constants/data/nft-red-packet.json +++ b/packages/web3-constants/data/nft-red-packet.json @@ -10,7 +10,8 @@ "Matic": "0xf6Dc042717EF4C097348bE00f4BaE688dcaDD4eA", "Mumbai": "", "Arbitrum": "", - "Arbitrum_Rinkeby": "" + "Arbitrum_Rinkeby": "", + "xDai": "" }, "SUBGRAPH_URL": { "Mainnet": "https://api.thegraph.com/subgraphs/name/dimensiondev/mask-nft-red-packet-mainnet", @@ -23,6 +24,7 @@ "Matic": "https://api.thegraph.com/subgraphs/name/dimensiondev/mask-nft-red-packet-polygon", "Mumbai": "", "Arbitrum": "", - "Arbitrum_Rinkeby": "" + "Arbitrum_Rinkeby": "", + "xDai": "" } } diff --git a/packages/web3-constants/data/space-station-galaxy.json b/packages/web3-constants/data/space-station-galaxy.json index aab6d40926ec..fa06ca4ca769 100644 --- a/packages/web3-constants/data/space-station-galaxy.json +++ b/packages/web3-constants/data/space-station-galaxy.json @@ -10,7 +10,8 @@ "Matic": "0xdeb1F826c512EEE2FA9398225A3401A0Dd5311E2", "Mumbai": "0x1e82cFbcF7292DEf91a2A5736265555883FF0289", "Arbitrum": "", - "Arbitrum_Rinkeby": "" + "Arbitrum_Rinkeby": "", + "xDai": "" }, "SUBGRAPH_URL": { "Mainnet": "", @@ -23,6 +24,7 @@ "Matic": "https://graphigo.prd.galaxy.eco/query", "Mumbai": "https://graphigo.stg.galaxy.eco/query", "Arbitrum": "", - "Arbitrum_Rinkeby": "" + "Arbitrum_Rinkeby": "", + "xDai": "" } } diff --git a/packages/web3-contracts/abis/MaskBox.json b/packages/web3-contracts/abis/MaskBox.json new file mode 100644 index 000000000000..07595d9e2e4d --- /dev/null +++ b/packages/web3-contracts/abis/MaskBox.json @@ -0,0 +1,436 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "token_address", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "ClaimPayment", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "nft_address", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "start_time", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "end_time", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "bool", + "name": "sell_all", + "type": "bool" + } + ], + "name": "CreationSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "customer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "nft_address", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "OpenSuccess", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "nft_id_list", + "type": "uint256[]" + } + ], + "name": "addNftIntoBox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "box_ids", + "type": "uint256[]" + } + ], + "name": "claimPayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "nft_address", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "components": [ + { + "internalType": "address", + "name": "token_addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "internalType": "struct MysteryBox.PaymentOption[]", + "name": "payment", + "type": "tuple[]" + }, + { + "internalType": "uint32", + "name": "personal_limit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "start_time", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "end_time", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "sell_all", + "type": "bool" + }, + { + "internalType": "uint256[]", + "name": "nft_id_list", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "qualification", + "type": "address" + } + ], + "name": "createBox", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + } + ], + "name": "getBoxInfo", + "outputs": [ + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "address", + "name": "nft_address", + "type": "address" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "components": [ + { + "internalType": "address", + "name": "token_addr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receivable_amount", + "type": "uint256" + } + ], + "internalType": "struct MysteryBox.PaymentInfo[]", + "name": "payment", + "type": "tuple[]" + }, + { + "internalType": "uint32", + "name": "personal_limit", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "started", + "type": "bool" + }, + { + "internalType": "bool", + "name": "expired", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "remaining", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "total", + "type": "uint256" + }, + { + "internalType": "address", + "name": "qualification", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "cursor", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "getNftListForSale", + "outputs": [ + { + "internalType": "uint256[]", + "name": "nft_id_list", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "customer", + "type": "address" + } + ], + "name": "getPurchasedNft", + "outputs": [ + { + "internalType": "uint256[]", + "name": "nft_id_list", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "box_id", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "amount", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "payment_token_index", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "openBox", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/packages/web3-contracts/types/MaskBox.d.ts b/packages/web3-contracts/types/MaskBox.d.ts new file mode 100644 index 000000000000..c766cdb4592a --- /dev/null +++ b/packages/web3-contracts/types/MaskBox.d.ts @@ -0,0 +1,165 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import BN from 'bn.js' +import { ContractOptions } from 'web3-eth-contract' +import { EventLog } from 'web3-core' +import { EventEmitter } from 'events' +import { + Callback, + PayableTransactionObject, + NonPayableTransactionObject, + BlockType, + ContractEventLog, + BaseContract, +} from './types' + +interface EventOptions { + filter?: object + fromBlock?: BlockType + topics?: string[] +} + +export type ClaimPayment = ContractEventLog<{ + creator: string + box_id: string + token_address: string + amount: string + timestamp: string + 0: string + 1: string + 2: string + 3: string + 4: string +}> +export type CreationSuccess = ContractEventLog<{ + creator: string + nft_address: string + box_id: string + name: string + start_time: string + end_time: string + sell_all: boolean + 0: string + 1: string + 2: string + 3: string + 4: string + 5: string + 6: boolean +}> +export type OpenSuccess = ContractEventLog<{ + box_id: string + customer: string + nft_address: string + amount: string + 0: string + 1: string + 2: string + 3: string +}> +export type OwnershipTransferred = ContractEventLog<{ + previousOwner: string + newOwner: string + 0: string + 1: string +}> + +export interface MaskBox extends BaseContract { + constructor(jsonInterface: any[], address?: string, options?: ContractOptions): MaskBox + clone(): MaskBox + methods: { + addNftIntoBox( + box_id: number | string | BN, + nft_id_list: (number | string | BN)[], + ): NonPayableTransactionObject + + claimPayment(box_ids: (number | string | BN)[]): NonPayableTransactionObject + + createBox( + nft_address: string, + name: string, + payment: [string, number | string | BN][], + personal_limit: number | string | BN, + start_time: number | string | BN, + end_time: number | string | BN, + sell_all: boolean, + nft_id_list: (number | string | BN)[], + qualification: string, + ): NonPayableTransactionObject + + getBoxInfo(box_id: number | string | BN): NonPayableTransactionObject<{ + creator: string + nft_address: string + name: string + payment: [string, string, string][] + personal_limit: string + started: boolean + expired: boolean + remaining: string + total: string + qualification: string + 0: string + 1: string + 2: string + 3: [string, string, string][] + 4: string + 5: boolean + 6: boolean + 7: string + 8: string + 9: string + }> + + getNftListForSale( + box_id: number | string | BN, + cursor: number | string | BN, + amount: number | string | BN, + ): NonPayableTransactionObject + + getPurchasedNft(box_id: number | string | BN, customer: string): NonPayableTransactionObject + + initialize(): NonPayableTransactionObject + + openBox( + box_id: number | string | BN, + amount: number | string | BN, + payment_token_index: number | string | BN, + proof: string | number[], + ): PayableTransactionObject + + owner(): NonPayableTransactionObject + + renounceOwnership(): NonPayableTransactionObject + + transferOwnership(newOwner: string): NonPayableTransactionObject + } + events: { + ClaimPayment(cb?: Callback): EventEmitter + ClaimPayment(options?: EventOptions, cb?: Callback): EventEmitter + + CreationSuccess(cb?: Callback): EventEmitter + CreationSuccess(options?: EventOptions, cb?: Callback): EventEmitter + + OpenSuccess(cb?: Callback): EventEmitter + OpenSuccess(options?: EventOptions, cb?: Callback): EventEmitter + + OwnershipTransferred(cb?: Callback): EventEmitter + OwnershipTransferred(options?: EventOptions, cb?: Callback): EventEmitter + + allEvents(options?: EventOptions, cb?: Callback): EventEmitter + } + + once(event: 'ClaimPayment', cb: Callback): void + once(event: 'ClaimPayment', options: EventOptions, cb: Callback): void + + once(event: 'CreationSuccess', cb: Callback): void + once(event: 'CreationSuccess', options: EventOptions, cb: Callback): void + + once(event: 'OpenSuccess', cb: Callback): void + once(event: 'OpenSuccess', options: EventOptions, cb: Callback): void + + once(event: 'OwnershipTransferred', cb: Callback): void + once(event: 'OwnershipTransferred', options: EventOptions, cb: Callback): void +} diff --git a/packages/web3-shared/src/constants/index.ts b/packages/web3-shared/src/constants/index.ts index c034ec1bdbe5..3e45107b5964 100644 --- a/packages/web3-shared/src/constants/index.ts +++ b/packages/web3-shared/src/constants/index.ts @@ -8,6 +8,7 @@ import NftRedPacket from '@masknet/web3-constants/data/nft-red-packet.json' import Token from '@masknet/web3-constants/data/token.json' import Trader from '@masknet/web3-constants/data/trader.json' import Trending from '@masknet/web3-constants/data/trending.json' +import MaskBox from '@masknet/web3-constants/data/mask-box.json' import RPC from '@masknet/web3-constants/data/rpc.json' import PoolTogether from '@masknet/web3-constants/data/pooltogether.json' import TokenAssetBaseURL from '@masknet/web3-constants/data/token-asset-base-url.json' @@ -43,7 +44,15 @@ export const useTraderConstants = hookTransform(getTraderConstants) export const getTrendingConstants = transform(Trending) export const useTrendingConstants = hookTransform(getTrendingConstants) -export const getRPCConstants = transformFromJSON(process.env.WEB3_CONSTANTS_RPC ?? '', RPC) +export const getMaskBoxConstants = transform(MaskBox) +export const useMaskBoxConstants = hookTransform(getMaskBoxConstants) + +let WEB3_CONSTANTS_RPC = '' +try { + WEB3_CONSTANTS_RPC = process.env.WEB3_CONSTANTS_RPC ?? '' +} catch {} + +export const getRPCConstants = transformFromJSON(WEB3_CONSTANTS_RPC, RPC) export const useRPCConstants = hookTransform(getRPCConstants) export const getTokenAssetBaseURLConstants = transform(TokenAssetBaseURL) diff --git a/packages/web3-shared/src/hooks/index.ts b/packages/web3-shared/src/hooks/index.ts index daf594e48c77..bff7b3eb8e53 100644 --- a/packages/web3-shared/src/hooks/index.ts +++ b/packages/web3-shared/src/hooks/index.ts @@ -43,8 +43,10 @@ export * from './useFungibleTokenWatched' export * from './useTokensBalance' export * from './useTransaction' export * from './useTransactionState' +export * from './useTransactionCallback' export * from './useWeb3' export * from './useNetworkType' +export * from './useProviderType' export * from './useAssetsFromChain' export * from './useAssetsMerged' export * from './useAssetsFromProvider' @@ -58,6 +60,7 @@ export * from './useChainColor' export * from './useResolveENS' export * from './useResolveUNS' export * from './useGasLimit' + // wallet plugin related export * from './useWallet' export * from './useWallets' diff --git a/packages/web3-shared/src/hooks/useContract.ts b/packages/web3-shared/src/hooks/useContract.ts index 3003e9dd80c7..c707c77d5a05 100644 --- a/packages/web3-shared/src/hooks/useContract.ts +++ b/packages/web3-shared/src/hooks/useContract.ts @@ -8,7 +8,7 @@ import { useWeb3 } from './useWeb3' export function createContract(web3: Web3, address: string, ABI: AbiItem[]) { if (!address || !EthereumAddress.isValid(address)) return null const contract = new web3.eth.Contract(ABI, address) as unknown as T - contract.transactionConfirmationBlocks = 1 + contract.transactionConfirmationBlocks = 0 contract.transactionPollingTimeout = 5000 return contract } diff --git a/packages/web3-shared/src/hooks/useERC721ContractDetailed.ts b/packages/web3-shared/src/hooks/useERC721ContractDetailed.ts index 3cf81a44a7bc..90daaf96460e 100644 --- a/packages/web3-shared/src/hooks/useERC721ContractDetailed.ts +++ b/packages/web3-shared/src/hooks/useERC721ContractDetailed.ts @@ -7,12 +7,12 @@ import { createERC721ContractDetailed, safeNonPayableTransactionCall } from '../ import type { ERC721 } from '../../../web3-contracts/types/ERC721' import { useOpenseaAPIConstants } from '../constants' -export function useERC721ContractDetailed(address: string) { +export function useERC721ContractDetailed(address?: string) { const chainId = useChainId() const { GET_CONTRACT_URL } = useOpenseaAPIConstants() const erc721TokenContract = useERC721TokenContract(address) return useAsyncRetry(async () => { - if (!EthereumAddress.isValid(address) || !erc721TokenContract) return + if (!address || !EthereumAddress.isValid(address) || !erc721TokenContract) return if (!GET_CONTRACT_URL) return getERC721ContractDetailedFromChain(address, chainId, erc721TokenContract) const contractDetailedFromOpensea = await getERC721ContractDetailedFromOpensea( address, @@ -21,7 +21,7 @@ export function useERC721ContractDetailed(address: string) { ) return contractDetailedFromOpensea ?? getERC721ContractDetailedFromChain(address, chainId, erc721TokenContract) - }, [chainId, erc721TokenContract, address]) + }, [address, chainId, erc721TokenContract]) } const lazyBlank = Promise.resolve('') diff --git a/packages/web3-shared/src/hooks/useERC721ContractIsApproveForAll.ts b/packages/web3-shared/src/hooks/useERC721ContractIsApproveForAll.ts index 7ca3a1095877..78e5d59d05b2 100644 --- a/packages/web3-shared/src/hooks/useERC721ContractIsApproveForAll.ts +++ b/packages/web3-shared/src/hooks/useERC721ContractIsApproveForAll.ts @@ -3,7 +3,7 @@ import { useERC721TokenContract } from '../contracts/useERC721TokenContract' import { safeNonPayableTransactionCall } from '../utils' /** - * @param contractAddress Nft contract address. + * @param contractAddress NFT contract address. * @param owner The address that owns the NFTs. * @param operator The address that acts on behalf of the owner. * @return True if `operator` is an approved operator for `owner`, false otherwise. diff --git a/packages/web3-shared/src/hooks/useERC721ContractSetApproveForAllCallback.ts b/packages/web3-shared/src/hooks/useERC721ContractSetApproveForAllCallback.ts index ea33f2a385de..13c49cd38aff 100644 --- a/packages/web3-shared/src/hooks/useERC721ContractSetApproveForAllCallback.ts +++ b/packages/web3-shared/src/hooks/useERC721ContractSetApproveForAllCallback.ts @@ -10,7 +10,7 @@ import { } from '../index' /** - * @param contractAddress Nft contract Address. + * @param contractAddress NFT contract Address. * @param operator Address to add to the set of authorized operators. * @param approved True if the operator is approved, false to revoke approval. */ diff --git a/packages/web3-shared/src/hooks/useProviderType.ts b/packages/web3-shared/src/hooks/useProviderType.ts new file mode 100644 index 000000000000..f8856581c122 --- /dev/null +++ b/packages/web3-shared/src/hooks/useProviderType.ts @@ -0,0 +1,5 @@ +import { useWeb3StateContext } from '..' + +export function useProviderType() { + return useWeb3StateContext().providerType +} diff --git a/packages/web3-shared/src/hooks/useTransactionCallback.ts b/packages/web3-shared/src/hooks/useTransactionCallback.ts new file mode 100644 index 000000000000..529165da196c --- /dev/null +++ b/packages/web3-shared/src/hooks/useTransactionCallback.ts @@ -0,0 +1,84 @@ +import { useCallback } from 'react' +import type { PayableTransactionObject, PayableTx } from '@masknet/web3-contracts/types/types' +import { TransactionStateType, useTransactionState } from '.' +import { TransactionEventType } from '..' + +export function useTransactionCallback( + type: TransactionStateType.HASH | TransactionStateType.RECEIPT | TransactionStateType.CONFIRMED, + config: PayableTx | undefined, + method: PayableTransactionObject | undefined, +) { + const [state, setState] = useTransactionState() + + const updateCallback = useCallback(async () => { + if (!config || !method) { + setState({ + type: TransactionStateType.UNKNOWN, + }) + return + } + + setState({ + type: TransactionStateType.WAIT_FOR_CONFIRMING, + }) + + try { + await method.estimateGas(config) + } catch (error) { + try { + await method.call(config) + } catch (error) { + setState({ + type: TransactionStateType.FAILED, + error: error instanceof Error ? error : new Error('Unknown Error.'), + }) + throw error + } + } + + return new Promise(async (resolve, reject) => { + method + .send(config) + .once(TransactionEventType.TRANSACTION_HASH, (hash) => { + if (type !== TransactionStateType.HASH) return + setState({ + type: TransactionStateType.HASH, + hash, + }) + resolve() + }) + .once(TransactionEventType.RECEIPT, (receipt) => { + if (type !== TransactionStateType.RECEIPT) return + setState({ + type: TransactionStateType.RECEIPT, + receipt, + }) + resolve() + }) + .once(TransactionEventType.CONFIRMATION, (no, receipt) => { + if (type !== TransactionStateType.CONFIRMED) return + setState({ + type: TransactionStateType.CONFIRMED, + no, + receipt, + }) + resolve() + }) + .once(TransactionEventType.ERROR, (error) => { + setState({ + type: TransactionStateType.FAILED, + error, + }) + reject(error) + }) + }) + }, [type, config, method]) + + const resetCallback = useCallback(() => { + setState({ + type: TransactionStateType.UNKNOWN, + }) + }, []) + + return [state, updateCallback, resetCallback] as const +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23fe9ed1adc5..a114ea24eed1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,6 +361,7 @@ importers: react-virtualized-auto-sizer: ^1.0.6 react-window: ^1.8.6 remarkable: ^2.0.1 + rss3-next: 0.6.17 safari-14-idb-fix: ^2.0.3 scrypt-js: ^3.0.1 socket.io-client: 2.4.0 @@ -495,6 +496,7 @@ importers: react-virtualized-auto-sizer: 1.0.6_f991c289939c8299bed5e4a6a5a79a62 react-window: 1.8.6_f991c289939c8299bed5e4a6a5a79a62 remarkable: 2.0.1 + rss3-next: 0.6.17_typescript@4.4.3 safari-14-idb-fix: 2.0.3 scrypt-js: 3.0.1 socket.io-client: 2.4.0 @@ -4487,6 +4489,14 @@ packages: '@ethersproject/properties': 5.4.0 dev: false + /@ethersproject/bignumber/5.4.0: + resolution: {integrity: sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==} + dependencies: + '@ethersproject/bytes': 5.4.0 + '@ethersproject/logger': 5.4.0 + bn.js: 4.12.0 + dev: false + /@ethersproject/bignumber/5.4.1: resolution: {integrity: sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==} dependencies: @@ -4634,6 +4644,33 @@ packages: - utf-8-validate dev: false + /@ethersproject/providers/5.4.1: + resolution: {integrity: sha512-p06eiFKz8nu/5Ju0kIX024gzEQIgE5pvvGrBCngpyVjpuLtUIWT3097Agw4mTn9/dEA0FMcfByzFqacBMSgCVg==} + dependencies: + '@ethersproject/abstract-provider': 5.4.0 + '@ethersproject/abstract-signer': 5.4.1 + '@ethersproject/address': 5.4.0 + '@ethersproject/basex': 5.4.0 + '@ethersproject/bignumber': 5.4.1 + '@ethersproject/bytes': 5.4.0 + '@ethersproject/constants': 5.4.0 + '@ethersproject/hash': 5.4.0 + '@ethersproject/logger': 5.4.0 + '@ethersproject/networks': 5.4.1 + '@ethersproject/properties': 5.4.0 + '@ethersproject/random': 5.4.0 + '@ethersproject/rlp': 5.4.0 + '@ethersproject/sha2': 5.4.0 + '@ethersproject/strings': 5.4.0 + '@ethersproject/transactions': 5.4.0 + '@ethersproject/web': 5.4.0 + bech32: 1.1.4 + ws: 7.4.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /@ethersproject/providers/5.4.2: resolution: {integrity: sha512-Qr8Am8hlj2gL9HwNymhFlYd52MQVVEBLoDwPxhv4ASeyNpaoRiUAQnNEuE6SnEQtiwYkpLrQtSALNLUSeyuvjA==} dependencies: @@ -9572,7 +9609,7 @@ packages: /axios/0.21.1: resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} dependencies: - follow-redirects: 1.14.1 + follow-redirects: 1.14.3 transitivePeerDependencies: - debug dev: false @@ -11804,6 +11841,10 @@ packages: resolution: {integrity: sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==} dev: false + /crypto-js/4.1.1: + resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==} + dev: false + /crypto-random-string/2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -13673,6 +13714,44 @@ packages: safe-buffer: 5.2.1 dev: false + /ethers/5.4.1: + resolution: {integrity: sha512-SrcddMdCgP1hukDvCPd87Aipbf4NWjQvdfAbZ65XSZGbfyuYPtIrUJPDH5B1SBRsdlfiEgX3eoz28DdBDzMNFg==} + dependencies: + '@ethersproject/abi': 5.4.0 + '@ethersproject/abstract-provider': 5.4.0 + '@ethersproject/abstract-signer': 5.4.0 + '@ethersproject/address': 5.4.0 + '@ethersproject/base64': 5.4.0 + '@ethersproject/basex': 5.4.0 + '@ethersproject/bignumber': 5.4.0 + '@ethersproject/bytes': 5.4.0 + '@ethersproject/constants': 5.4.0 + '@ethersproject/contracts': 5.4.0 + '@ethersproject/hash': 5.4.0 + '@ethersproject/hdnode': 5.4.0 + '@ethersproject/json-wallets': 5.4.0 + '@ethersproject/keccak256': 5.4.0 + '@ethersproject/logger': 5.4.0 + '@ethersproject/networks': 5.4.1 + '@ethersproject/pbkdf2': 5.4.0 + '@ethersproject/properties': 5.4.0 + '@ethersproject/providers': 5.4.1 + '@ethersproject/random': 5.4.0 + '@ethersproject/rlp': 5.4.0 + '@ethersproject/sha2': 5.4.0 + '@ethersproject/signing-key': 5.4.0 + '@ethersproject/solidity': 5.4.0 + '@ethersproject/strings': 5.4.0 + '@ethersproject/transactions': 5.4.0 + '@ethersproject/units': 5.4.0 + '@ethersproject/wallet': 5.4.0 + '@ethersproject/web': 5.4.0 + '@ethersproject/wordlists': 5.4.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /ethjs-unit/0.1.6: resolution: {integrity: sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=} engines: {node: '>=6.5.0', npm: '>=3'} @@ -14232,16 +14311,6 @@ packages: readable-stream: 2.3.7 dev: true - /follow-redirects/1.14.1: - resolution: {integrity: sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - /follow-redirects/1.14.3: resolution: {integrity: sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==} engines: {node: '>=4.0'} @@ -16860,6 +16929,11 @@ packages: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} dev: false + /js-cookie/3.0.1: + resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==} + engines: {node: '>=12'} + dev: false + /js-sha256/0.9.0: resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} dev: false @@ -18220,7 +18294,6 @@ packages: /nested-error-stacks/2.1.0: resolution: {integrity: sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==} - dev: true /next-tick/1.0.0: resolution: {integrity: sha1-yobR/ogoFpsBICCOPchCS524NCw=} @@ -20254,6 +20327,12 @@ packages: indent-string: 4.0.0 strip-indent: 3.0.0 + /reflect-metadata/0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + requiresBuild: true + dev: false + optional: true + /refractor/3.4.0: resolution: {integrity: sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg==} dependencies: @@ -20712,6 +20791,25 @@ packages: fsevents: 2.3.2 dev: true + /rss3-next/0.6.17_typescript@4.4.3: + resolution: {integrity: sha512-UEVpr9somInaz8kreZjUjVmo0cay/Q24CQSo8QJogChlphUpFyMAJSXtC7xS2p67ZSXL9CWPtzoPZtQ4wh0hBg==} + dependencies: + axios: 0.21.1 + buffer: 6.0.3 + crypto-js: 4.1.1 + ethers: 5.4.1 + js-cookie: 3.0.1 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + typescript-is: 0.18.2_typescript@4.4.3 + util: 0.12.4 + transitivePeerDependencies: + - bufferutil + - debug + - typescript + - utf-8-validate + dev: false + /rsvp/4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -22619,7 +22717,6 @@ packages: dependencies: tslib: 1.14.1 typescript: 4.4.3 - dev: true /tty-browserify/0.0.0: resolution: {integrity: sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=} @@ -22630,10 +22727,18 @@ packages: safe-buffer: 5.2.1 dev: false + /tweetnacl-util/0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + dev: false + /tweetnacl/0.14.5: resolution: {integrity: sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=} dev: false + /tweetnacl/1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + dev: false + /type-check/0.3.2: resolution: {integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=} engines: {node: '>= 0.8.0'} @@ -22750,6 +22855,19 @@ packages: typescript: 2.9.2 dev: false + /typescript-is/0.18.2_typescript@4.4.3: + resolution: {integrity: sha512-TXgcmHLbuYaKtwyGy/v+oFWi9sIlEUSMcHSuRbIH7OtUuaMP6/bOPwOMDuEVRtGQ62Yx1XMdxFGbax81nXDKjg==} + engines: {node: '>=6.14.4'} + peerDependencies: + typescript: ^4.1.5 + dependencies: + nested-error-stacks: 2.1.0 + tsutils: 3.21.0_typescript@4.4.3 + typescript: 4.4.3 + optionalDependencies: + reflect-metadata: 0.1.13 + dev: false + /typescript/2.9.2: resolution: {integrity: sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==} engines: {node: '>=4.2.0'} @@ -24540,19 +24658,6 @@ packages: optional: true dev: false - /ws/7.5.4: - resolution: {integrity: sha512-zP9z6GXm6zC27YtspwH99T3qTG7bBFv2VIkeHstMLrLlDJuzA7tQ5ls3OJ1hOGGCzTQPniNJoHXIAOS0Jljohg==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - /ws/7.5.5: resolution: {integrity: sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==} engines: {node: '>=8.3.0'}