diff --git a/src/appGlobals.ts b/src/appGlobals.ts index 38533d35c..b0fb81985 100644 --- a/src/appGlobals.ts +++ b/src/appGlobals.ts @@ -1,4 +1,29 @@ -const scrollRequest = (url: string, options?: object) => { +const scrollRequest = (url: string, options?: RequestInit & { timeout?: number }) => { + if (options?.timeout) { + const controller = new AbortController() + const { signal } = controller + const optionsWithSignal = { ...options, signal } + + const timeoutId = setTimeout(() => { + controller.abort() + }, options.timeout) + + return fetch(url, optionsWithSignal) + .then(async res => { + if (res.ok) { + clearTimeout(timeoutId) + return res.json() + } + // server response but not 200 + const message = await res.text() + const error = new Error(message) + error.status = res.status + clearTimeout(timeoutId) + throw error + }) + .then(data => data) + } + return fetch(url, options) .then(async res => { if (res.ok) { diff --git a/src/assets/abis/CanvasBadge.json b/src/assets/abis/CanvasBadge.json index 08422a3f7..6c19c6947 100644 --- a/src/assets/abis/CanvasBadge.json +++ b/src/assets/abis/CanvasBadge.json @@ -67,5 +67,22 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "name": "recipient", + "type": "address" + } + ], + "name": "isEligible", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" } ] diff --git a/src/constants/badge.ts b/src/constants/badge.ts index 600cf26c7..ad366facb 100644 --- a/src/constants/badge.ts +++ b/src/constants/badge.ts @@ -20,9 +20,10 @@ export interface Badge { } badgeContract: string - // third party + // Backend-authorized attesterProxy?: string - eligibilityAPI?: string + baseUrl?: string + eligibilityCheck?: boolean // Origin NFT originsNFT?: boolean @@ -30,8 +31,10 @@ export interface Badge { nftAddress?: string[] nftAbi?: object - // view third party badge website + // issued by Scroll native: boolean + + airdrop?: boolean } // TODO: only keep OriginsNFTBadge and EthereumYearBadge @@ -93,6 +96,7 @@ export const EAMPLE_BADGES = [ description: "A collection 8888 Cute Chubby Pudgy Penquins sliding around on the freezing ETH blockchain.", image: "/imgs/canvas/Penguin1.webp", native: true, + eligibilityCheck: true, issuer: { origin: "https://scroll.io", name: "Scroll", diff --git a/src/pages/canvas/Dashboard/BadgeDetailDialog/index.tsx b/src/pages/canvas/Dashboard/BadgeDetailDialog/index.tsx index b0e1766db..129aee377 100644 --- a/src/pages/canvas/Dashboard/BadgeDetailDialog/index.tsx +++ b/src/pages/canvas/Dashboard/BadgeDetailDialog/index.tsx @@ -49,6 +49,9 @@ const ButtonContainer = styled(forwardRef((props, ref) => { } } - const renderButtonText = () => { - if (isMobile && selectedBadge.airdrop) { - return "Waiting for issuer to mint" - } else if (isBadgeMinting.get(selectedBadge.badgeContract)) { - return "Minting" - } - return "Mint badge" + const renderMintTip = () => { + return ( + <> + {[BadgeDetailDialogType.NO_PROFILE].includes(badgeDetailDialogVisible) && ( + + + + You need a Scroll Canvas in order to mint your {selectedBadge.name} Badge. + + + )} + {[BadgeDetailDialogType.MINT, BadgeDetailDialogType.MINT_WITH_BACK].includes(badgeDetailDialogVisible) && selectedBadge.airdrop && ( + + + + You are eligible. Your badge will be airdroped by the issuer. + + + )} + + ) } return ( @@ -248,15 +265,7 @@ const BadgeDetailDialog = () => { )} - - {[BadgeDetailDialogType.NO_PROFILE].includes(badgeDetailDialogVisible) && ( - - - - You need a Scroll Canvas in order to mint your {selectedBadge.name} Badge. - - - )} + {!isMobile && renderMintTip()} {[BadgeDetailDialogType.MINT, BadgeDetailDialogType.MINT_WITH_BACK].includes(badgeDetailDialogVisible) && ( @@ -267,31 +276,10 @@ const BadgeDetailDialog = () => { color="primary" onClick={handleMint} > - {renderButtonText()} + {isBadgeMinting.get(selectedBadge.badgeContract) ? "Minting" : "Mint badge"} )} - {!isMobile && selectedBadge.airdrop && ( - - This is an airdrop-only badge and you will receive it once the issuer mint for you. - - )} - - {/* {[BadgeDetailDialogType.UPGRADE].includes(badgeDetailDialogVisible) && ( - - View on EAS - - )} */} + {isMobile && renderMintTip()} {[BadgeDetailDialogType.NO_PROFILE].includes(badgeDetailDialogVisible) && ( diff --git a/src/pages/canvas/badgeContract/index.tsx b/src/pages/canvas/badgeContract/index.tsx index 9d8bba281..5714eb902 100644 --- a/src/pages/canvas/badgeContract/index.tsx +++ b/src/pages/canvas/badgeContract/index.tsx @@ -124,7 +124,17 @@ const BadgeContractDetail = props => { ) - } else if (profileMinted && isOwned === false && isEligible && !badgeForMint.airdrop) { + } else if (profileMinted && isOwned === false && isEligible) { + if (badgeForMint.airdrop) { + return ( + <> + + + You are eligible. Your badge will be airdroped by the issuer. + + + ) + } return ( <> @@ -133,16 +143,17 @@ const BadgeContractDetail = props => { ) - } else if (profileMinted && isOwned === false && isEligible && badgeForMint.airdrop) { - return ( - <> - - - This is an airdrop-only badge and you will receive it once the issuer mint for you. - - - ) } else if (profileMinted && isOwned === false && !isEligible) { + if (badgeForMint.airdrop) { + return ( + <> + + + This is an airdrop-only badge. Selected account is not eligible. + + + ) + } return ( <> diff --git a/src/services/canvasService.ts b/src/services/canvasService.ts index 75dd49fee..57b9b5b49 100644 --- a/src/services/canvasService.ts +++ b/src/services/canvasService.ts @@ -255,14 +255,20 @@ const checkBadgeEligibility = async (provider, walletAddress, badge: any) => { const eligibility = await badge.validator(provider, walletAddress) return eligibility } - - // scroll native - if (badge.native) { + if (!badge.baseUrl && !badge.eligibilityCheck) { return true } - // third-party badge - if (badge.attesterProxy) { - const data = await scrollRequest(checkBadgeEligibilityURL(badge.baseUrl, walletAddress, badge.badgeContract)) + // permissionless + if (!badge.baseUrl && badge.eligibilityCheck) { + const badgeInstance = new ethers.Contract(badge.badgeContract, BadgeABI, provider) + const eligibility = await badgeInstance.isEligible(walletAddress) + return eligibility + } + // backend authorized / airdropped + if (badge.baseUrl) { + const data = await scrollRequest(checkBadgeEligibilityURL(badge.baseUrl, walletAddress, badge.badgeContract), { + timeout: 1e4, + }) // TODO: must return true or false return data.eligibility ?? false } @@ -273,7 +279,7 @@ const checkBadgeEligibility = async (provider, walletAddress, badge: any) => { } } -const mintThirdBadge = async (signer, walletAddress, badgeAddress, attesterProxyAddress, claimBaseUrl) => { +const mintBackendAuthorizedBadge = async (signer, walletAddress, badgeAddress, attesterProxyAddress, claimBaseUrl) => { const { tx: unsignedTx } = await scrollRequest(claimBadgeURL(claimBaseUrl, walletAddress, badgeAddress)) console.log(unsignedTx, "unsignedTx") checkDelegatedAttestation(unsignedTx, attesterProxyAddress) @@ -359,7 +365,7 @@ const mintBadge = async (provider, walletCurrentAddress, badge) => { } // Third Party Badge if (attesterProxy) { - return await mintThirdBadge(signer, walletCurrentAddress, badgeContract, attesterProxy, baseUrl) + return await mintBackendAuthorizedBadge(signer, walletCurrentAddress, badgeContract, attesterProxy, baseUrl) } return await mintPermissionlessBadge(signer, walletCurrentAddress, badgeContract)