diff --git a/ui/src/integrations/ethereum/lib/fleek-erc721.ts b/ui/src/integrations/ethereum/lib/fleek-erc721.ts index 220cac7..905381e 100644 --- a/ui/src/integrations/ethereum/lib/fleek-erc721.ts +++ b/ui/src/integrations/ethereum/lib/fleek-erc721.ts @@ -1,13 +1,27 @@ +import { + ErrorDescription as InterfaceErrorDescription, + Result as InterfaceResult, +} from '@ethersproject/abi/lib/interface'; +import { BytesLike } from 'ethers'; import { Ethereum } from '../ethereum'; +enum CollectionRoles { + Owner, + Verifier, +} + +enum TokenRoles { + Controller, +} + export const FleekERC721 = { + contract: Ethereum.getContract('FleekERC721'), + async mint( params: FleekERC721.MintParams, provider: Ethereum.Providers ): Promise { - const contract = Ethereum.getContract('FleekERC721', provider); - - const response = await contract.mint( + const response = await this.contract.connect(provider).mint( params.owner, params.name, params.description.replaceAll(/\n/g, '\\n'), //replace break lines with \\n so it doesn't break the json, @@ -39,6 +53,83 @@ export const FleekERC721 = { // TODO: fetch last token id return 7; }, + + parseError(error: BytesLike): FleekERC721.TransactionError { + try { + if (!error) throw new Error('Empty error'); + + const description = this.contract.interface.parseError(error); + const result = this.contract.interface.decodeErrorResult( + description.signature, + error + ); + + let message: string; + + switch (description.signature) { + case 'ContractIsNotPausable()': + message = 'This contract is not pausable'; + break; + + case 'ContractIsNotPaused()': + message = 'This contract is not paused'; + break; + + case 'ContractIsPaused()': + message = 'This contract is paused'; + break; + + case 'MustBeTokenOwner(uint256)': + message = `You must be the token #${result.tokenId} owner`; + break; + + case 'MustHaveAtLeastOneOwner()': + message = 'You must have at least one owner'; + break; + + case 'MustHaveCollectionRole(uint8)': + message = `You must have a collection role "${ + CollectionRoles[result.role] + }" to mint`; + break; + + case 'MustHaveTokenRole(uint256,uint8)': + message = `You must have a token role "${ + TokenRoles[result.role] + }" on token #${result.tokenId}`; + break; + + case 'PausableIsSetTo(bool)': + message = `Pausable is set to "${result.isPausable}"`; + break; + + case 'RoleAlreadySet()': + message = `Role is already set`; + break; + + case 'ThereIsNoTokenMinted()': + message = `There is no token minted`; + break; + + default: + message = 'Unknown error'; + } + + return { + message, + description, + result, + isIdentified: true, + }; + } catch { + return { + message: 'Unknown error', + description: null, + result: null, + isIdentified: false, + }; + } + }, }; export namespace FleekERC721 { @@ -61,4 +152,11 @@ export namespace FleekERC721 { } ]; }; + + export type TransactionError = { + message: string; + description: InterfaceErrorDescription | null; + result: InterfaceResult | null; + isIdentified: boolean; + }; } diff --git a/ui/src/views/mint/mint.context.tsx b/ui/src/views/mint/mint.context.tsx index 07c57b8..7cd4ff5 100644 --- a/ui/src/views/mint/mint.context.tsx +++ b/ui/src/views/mint/mint.context.tsx @@ -1,9 +1,10 @@ -import { ComboboxItem, DropdownItem } from '@/components'; -import { GithubState } from '@/store'; -import { EthereumHooks } from '@/integrations'; -import { createContext } from '@/utils'; import { useState } from 'react'; +import { ComboboxItem, DropdownItem } from '@/components'; +import { Ethereum, EthereumHooks } from '@/integrations'; +import { GithubState } from '@/store'; +import { createContext } from '@/utils'; + export type MintContext = { selectedUserOrg: ComboboxItem; repositoryName: GithubState.Repository; diff --git a/ui/src/views/mint/nft-card/nft-card.tsx b/ui/src/views/mint/nft-card/nft-card.tsx index 058f739..7533c58 100644 --- a/ui/src/views/mint/nft-card/nft-card.tsx +++ b/ui/src/views/mint/nft-card/nft-card.tsx @@ -9,7 +9,7 @@ type NftCardProps = { message: string; buttonText: string; leftIconButton?: React.ReactNode; - onClick: () => void; + onClick?: () => void; isLoading: boolean; }; @@ -53,7 +53,7 @@ export const NftCard: React.FC = ({ onClick={onClick} leftIcon={leftIconButton} isLoading={isLoading} - isDisabled={isLoading} + isDisabled={isLoading || !onClick} > {buttonText} diff --git a/ui/src/views/mint/preview-step/mint-preview.tsx b/ui/src/views/mint/preview-step/mint-preview.tsx index 14d50ea..13e8a75 100644 --- a/ui/src/views/mint/preview-step/mint-preview.tsx +++ b/ui/src/views/mint/preview-step/mint-preview.tsx @@ -1,5 +1,6 @@ import { Icon, IconButton, Stepper } from '@/components'; import { useTransactionCost } from '@/hooks'; +import { FleekERC721 } from '@/integrations'; import { Mint } from '@/views/mint/mint.context'; import { ethers } from 'ethers'; import { useMemo } from 'react'; @@ -8,7 +9,7 @@ import { NftCard } from '../nft-card'; export const MintPreview = () => { const { prevStep } = Stepper.useContext(); const { - prepare: { status: prepareStatus, data: prepareData }, + prepare: { status: prepareStatus, data: prepareData, error: prepareError }, write: { status: writeStatus, write }, transaction: { status: transactionStatus }, } = Mint.useTransactionContext(); @@ -24,6 +25,18 @@ export const MintPreview = () => { if (isCostLoading || prepareStatus === 'loading') return 'Calculating cost...'; + // TODO: better UI for prepare errors + if (prepareError) { + const parsedError = FleekERC721.parseError( + (prepareError as any).error?.data.data + ); + if (parsedError.isIdentified) { + return parsedError.message; + } + + return 'An error occurred while preparing the transaction'; + } + const formattedCost = ethers.utils.formatEther(cost).slice(0, 9); return `Minting this NFA will cost ${formattedCost} ${currency}.`; }, [prepareData, isCostLoading, prepareStatus]); @@ -59,7 +72,7 @@ export const MintPreview = () => { } message={message} buttonText="Mint NFA" - onClick={write!} + onClick={write} isLoading={isLoading} /> );