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 (
+
+
+
+ )
+}
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 (
+
+ )
+}
\ 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
}