diff --git a/ui/.env.production b/ui/.env.production index 2950093..15bc055 100644 --- a/ui/.env.production +++ b/ui/.env.production @@ -7,6 +7,7 @@ NEXT_PUBLIC_FLOWSCAN_URL=https://flowscan.org NEXT_PUBLIC_NFTCATALOG_ADDRESS=0x49a7cda3a1eecc29 NEXT_PUBLIC_NON_FUNGIBLE_TOKEN_ADDRESS=0x1d7e57aa55817448 +NEXT_PUBLIC_METADATAVIEWS_ADDRESS=0x1d7e57aa55817448 NEXT_PUBLIC_EMERALD_BOT_VERIFIERS_ADDRESS=0x129fee333390875e DISCORD_CLIENT_ID=907407354427998279 diff --git a/ui/components/BasicVerifierCreator.js b/ui/components/BasicVerifierCreator.js index 48e5dae..6dfedcc 100644 --- a/ui/components/BasicVerifierCreator.js +++ b/ui/components/BasicVerifierCreator.js @@ -6,12 +6,16 @@ import { useRecoilState } from "recoil" import { transactionInProgressState, } from "../lib/atoms" +import { PlusIcon } from "@heroicons/react/outline"; +import TraitFilterModal from "./TraitFilterModal"; export default function BasicVerifierCreator(props) { const [transactionInProgress,] = useRecoilState(transactionInProgressState) - const { index, verifierInfo, updateVerifierParam, updateNFTCatalogVerifier } = props + const { index, verifierInfo, updateVerifierParam, updateNFTCatalogVerifier, updateVerifierTraits} = props const [selectedNFT, setSelectedNFT] = useState(null) + const [traitFilterOpen, setTraitFilterOpen] = useState(false) + const [logo, setLogo] = useState(verifierInfo.logo) const [name, setName] = useState(verifierInfo.name) const [description, setDescription] = useState(verifierInfo.description) @@ -62,7 +66,7 @@ export default function BasicVerifierCreator(props) { -
+
+
+ ) } \ No newline at end of file diff --git a/ui/components/BasicVerifierView.js b/ui/components/BasicVerifierView.js index 754bcb0..31e1205 100644 --- a/ui/components/BasicVerifierView.js +++ b/ui/components/BasicVerifierView.js @@ -3,11 +3,11 @@ import BasicVerifierCreator from "./BasicVerifierCreator"; import PresetBasicVerifier from "./PresetBasicVerifier"; export default function BasicVerifierView(props) { - const { isPreset, verifierInfo, index, updateNFTCatalogVerifier, updateVerifierParam, deleteVerifier } = props + const { isPreset, verifierInfo, index, updateNFTCatalogVerifier, updateVerifierParam, deleteVerifier, updateVerifierTraits } = props return (
-
+
{ verifierInfo.parameters.length > 0 ? verifierInfo.parameters.map((parameter) => { diff --git a/ui/components/RoleVerifierCreator.js b/ui/components/RoleVerifierCreator.js index b837d22..37b4550 100644 --- a/ui/components/RoleVerifierCreator.js +++ b/ui/components/RoleVerifierCreator.js @@ -1,18 +1,30 @@ import BasicVerifierSelector from "./BasicVerifierSelector" import BasicVerifierView from "./BasicVerifierView" import { catalogTemplate } from "../flow/preset_verifiers" +import { TraitsLogic } from "./LogicSelector" +import { useState } from "react" export default function RoleVerifierCreator(props) { const { basicVerifiers: verifiers, setBasicVerifiers: setVerifiers } = props + const [verifierID, setVerifierID] = useState(verifiers.length) + const createNewVerifier = () => { const verifier = Object.assign({}, catalogTemplate) + verifier.id = verifierID + setVerifierID(verifierID + 1) + verifier.isPreset = false + verifier.traits = [] + verifier.traitsLogic = TraitsLogic.AND setVerifiers(oldVerifiers => [...oldVerifiers, verifier]) } const createPresetVerifier = (verifierInfo) => { const verifier = Object.assign({}, verifierInfo) + verifier.id = verifierID + setVerifierID(verifierID + 1) + verifier.isPreset = true setVerifiers(oldVerifiers => [...oldVerifiers, verifier]) } @@ -56,6 +68,18 @@ export default function RoleVerifierCreator(props) { }) } + const updateVerifierTraits = (index, traits, traitsLogic) => { + setVerifiers(oldVerifiers => { + const newVerifiers = oldVerifiers.map((verifier, idx) => { + if (idx == index) { + return { ...verifier, traits: traits, traitsLogic: traitsLogic} + } + return verifier + }) + return newVerifiers + }) + } + return (
@@ -70,7 +94,7 @@ export default function RoleVerifierCreator(props) { if (verifier.isPreset) { return ( diff --git a/ui/components/RoleVerifierCreatorSlideOver.js b/ui/components/RoleVerifierCreatorSlideOver.js index 038d475..68cd849 100644 --- a/ui/components/RoleVerifierCreatorSlideOver.js +++ b/ui/components/RoleVerifierCreatorSlideOver.js @@ -84,6 +84,7 @@ export default function RoleVerifierCreatorSlideOver(props) { { + if (initTraits.length == 0) { + return [{ id: traitID, trait: "", value: "" }] + } + return initTraits +} + +const getTraitsIDDefaultValue = (initTraits) => { + let id = 0 + for (let i = 0; i < initTraits.length; i++) { + const t = initTraits[i] + if (t.id > id) { + id = t.id + } + } + return id + 1 +} + +export default function TraitFilterModal(props) { + const [, setShowBasicNotification] = useRecoilState(showBasicNotificationState) + const [, setBasicNotificationContent] = useRecoilState(basicNotificationContentState) + + const [traits, setTraits] = useState([]) + + const { open, setOpen, name, index, updateVerifierTraits, initTraits, initTraitsLogic } = props + + const [traitID, setTraitID] = useState(getTraitsIDDefaultValue(initTraits)) + const [traitsLogic, setTraitsLogic] = useState(initTraitsLogic) + + useEffect(() => { + if (open) { + const defaultTraits = getTraitsDefaultValue(initTraits, traitID) + setTraits(defaultTraits) + } + }, [open]) + + return ( + + + +
+ + +
+
+
+ + +
+
+
+
+ {name} +
+ +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ ) +} diff --git a/ui/components/TraitInput.js b/ui/components/TraitInput.js new file mode 100644 index 0000000..c8acfaf --- /dev/null +++ b/ui/components/TraitInput.js @@ -0,0 +1,50 @@ +import { XIcon } from '@heroicons/react/outline' +import { useEffect, useState } from 'react' + +export default function TraitInput(props) { + const {index, trait, value, deleteTrait, updateTrait, deleteEnabled} = props + + const [traitName, setTraitName] = useState(trait) + const [traitValue, setTraitValue] = useState(value) + + useEffect(() => { + setTraitName(trait) + setTraitValue(value) + }, [index]) + + return ( +
+
+ { + updateTrait(index, event.target.value, value) + }} + /> + { + updateTrait(index, trait,event.target.value) + }} + /> +
+ +
+ ) +} \ No newline at end of file diff --git a/ui/components/TraitsEditor.js b/ui/components/TraitsEditor.js new file mode 100644 index 0000000..475c111 --- /dev/null +++ b/ui/components/TraitsEditor.js @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import TraitInput from "./TraitInput"; +import { useRecoilState } from "recoil" +import { + showBasicNotificationState, + basicNotificationContentState, +} from "../lib/atoms" + +export default function TraitsEditor(props) { + const [, setShowBasicNotification] = useRecoilState(showBasicNotificationState) + const [, setBasicNotificationContent] = useRecoilState(basicNotificationContentState) + + const [deleteEnabled, setDeleteEnabled] = useState(false) + + const { traits, setTraits, traitID, setTraitID } = props + + const updateTrait = (index, traitName, value) => { + setTraits(oldTraits => { + const newTraits = oldTraits.map((trait, idx) => { + if (idx == index) { + return {...trait, trait: traitName, value: value} + } + return trait + }) + return newTraits + }) + } + + const deleteTrait = (index) => { + if (traits.length == 1) { + return + } + const newTraits = traits.filter((trait, idx) => { + return idx != index + }) + setTraits(newTraits) + } + + useEffect(() => { + if (traits.length == 1) { + setDeleteEnabled(false) + } else { + setDeleteEnabled(true) + } + }, [traits]) + + return ( +
+
+
+ + +
+ + { + traits.map((t, index) => { + return ( + + ) + }) + } + +
+ ) +} \ No newline at end of file diff --git a/ui/flow/scripts.js b/ui/flow/scripts.js index b414fad..21d0321 100644 --- a/ui/flow/scripts.js +++ b/ui/flow/scripts.js @@ -21,7 +21,7 @@ const splitList = (list, chunkSize) => { export const bulkGetNftCatalog = async () => { const collectionIdentifiers = await getCollectionIdentifiers() - const groups = splitList(collectionIdentifiers, 50) + const groups = splitList(collectionIdentifiers, 40) const promises = groups.map((group) => { return getNftCatalogByCollectionIDs(group) }) diff --git a/ui/lib/utils.js b/ui/lib/utils.js index 357475c..2a0ca2a 100644 --- a/ui/lib/utils.js +++ b/ui/lib/utils.js @@ -1,3 +1,4 @@ +import { TraitsLogic } from "../components/LogicSelector" import { ModeShortCircuit } from "../components/VerificationModeSelector" import publicConfig from "../publicConfig" @@ -24,20 +25,82 @@ const findPublicInterface = (restrictions, contractAddress, contractName) => { return 'NonFungibleToken.CollectionPublic'; } +const generateTraitsVerificationCode = (traits, traitsLogic) => { + if (traits.length == 0) { + return `var traitsCheckPassed = true` + } + + let code = ` + var traitsCheckPassed = false + var checksCount = ${traits.length} + if checksCount == 0 { + traitsCheckPassed = true + } + for trait in view.traits { + ` + for (let i = 0; i < traits.length; i++) { + let t = traits[i] + let snippet = `` + if (traitsLogic == TraitsLogic.AND) { + snippet = ` + if trait.name == "${t.trait}" && (trait.value as? String) == "${t.value}" { + checksCount = checksCount - 1 + if checksCount == 0 { + traitsCheckPassed = true + break + } + continue + } + ` + } else { + snippet = ` + if trait.name == "${t.trait}" && (trait.value as? String) == "${t.value}" { + traitsCheckPassed = true + break + } + ` + } + code += snippet + } + + code += ` + } + ` + return code +} + const generateImportsAndScript = (basicVerifier) => { if (!basicVerifier.isPreset && basicVerifier.name == "Owns _ NFT(s)") { - const nft = basicVerifier.nft; + const nft = basicVerifier.nft const publicInterface = findPublicInterface(nft.collectionData.publicLinkedType.restrictions, nft.contractAddress, nft.contractName); const publicPath = `/${nft.collectionData.publicPath.domain}/${nft.collectionData.publicPath.identifier}` const imports = [ `import ${nft.contractName} from ${nft.contractAddress}`, - `import NonFungibleToken from ${publicConfig.nonFungibleTokenAddress}` + `import NonFungibleToken from ${publicConfig.nonFungibleTokenAddress}`, + `import MetadataViews from ${publicConfig.metadataViewsAddress}` ] + const traitsCheckScript = generateTraitsVerificationCode(basicVerifier.traits, basicVerifier.traitsLogic) const script = ` - if let collection = getAccount(user).getCapability(${publicPath}).borrow<&{${publicInterface}}>() { - let amount: Int = AMOUNT - if collection.getIDs().length >= amount { - SUCCESS + if let collection = getAccount(user).getCapability(${publicPath}).borrow<&{${publicInterface}, MetadataViews.ResolverCollection}>() { + var amount: Int = 0 + let traitsLength = ${basicVerifier.traits.length} + for id in collection.getIDs() { + let resolver = collection.borrowViewResolver(id: id) + if let _view = resolver.resolveView(Type()) { + let view = _view as! MetadataViews.Traits + ${traitsCheckScript} + if traitsCheckPassed { + amount = amount + 1 + } + } else if traitsLength == 0 { + amount = amount + 1 + } else { + panic("No traits view found") + } + if amount >= AMOUNT { + SUCCESS + break + } } } `; @@ -104,7 +167,7 @@ export const generateScript = (roleVerifiers, verificationMode) => { } const verifyScript = ` - import EmeraldIdentity from 0x39e42c67cc851cfb +import EmeraldIdentity from 0x39e42c67cc851cfb ${imports.join('\n')} pub fun main(discordIds: [String]): {String: [String]} { diff --git a/ui/publicConfig.js b/ui/publicConfig.js index 6b2dbdd..b945554 100644 --- a/ui/publicConfig.js +++ b/ui/publicConfig.js @@ -19,6 +19,9 @@ if (!nftCatalogAddress) throw "Missing NEXT_PUBLIC_NFTCATALOG_ADDRESS" const nonFungibleTokenAddress = process.env.NEXT_PUBLIC_NON_FUNGIBLE_TOKEN_ADDRESS if (!nonFungibleTokenAddress) throw "Missing NEXT_PUBLIC_NON_FUNGIBLE_TOKEN_ADDRESS" +const metadataViewsAddress = process.env.NEXT_PUBLIC_METADATAVIEWS_ADDRESS +if (!metadataViewsAddress) throw "Missing NEXT_PUBLIC_METADATAVIEWS_ADDRESS" + const emeraldBotVerifiersAddress = process.env.NEXT_PUBLIC_EMERALD_BOT_VERIFIERS_ADDRESS if (!emeraldBotVerifiersAddress) throw "Missing NEXT_PUBLIC_EMERALD_BOT_VERIFIERS_ADDRESS" @@ -32,6 +35,7 @@ const publicConfig = { flowscanURL, nftCatalogAddress, nonFungibleTokenAddress, + metadataViewsAddress, emeraldBotVerifiersAddress, imageSizeLimit }