From 8309a12315e35630a2f42aaa5f9cc41f02fd5b7a Mon Sep 17 00:00:00 2001 From: memelotsqui Date: Tue, 2 Jul 2024 00:03:33 -0600 Subject: [PATCH 1/4] add wallet integration --- public/manifest.json | 24 ++-- src/library/CharacterManifestData.js | 107 ++++++++++---- src/library/characterManager.js | 10 +- src/library/mint-utils.js | 45 +++++- src/pages/Create.jsx | 204 ++++++++++++++++++++++++++- src/pages/Create.module.css | 37 ++++- src/pages/Landing.jsx | 7 +- src/pages/Wallet.jsx | 6 +- 8 files changed, 373 insertions(+), 67 deletions(-) diff --git a/public/manifest.json b/public/manifest.json index 3c8b1f2b..550e90a1 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -2,27 +2,23 @@ "characters":[ { "name": "Anata", - "description": "Anata", + "description": "Owned Customizer", "portrait": "./assets/portraitImages/anata.png", "manifest":"./character-assets/anata/manifest.json", "icon": "./assets/icons/class-neural-hacker.svg", - "format": "vrm" + "format": "vrm", + "collectionLock":"the-anata-nft" + }, { - "name": "Anata Male", - "description": "Anata Male", + "name": "Anata", + "description": "Full Customizer", "portrait": "./assets/portraitImages/anata_male.png", "manifest":"./character-assets/anata_male/manifest.json", - "icon": "|", - "format": "vrm" - }, - { - "name": "Anata Male", - "description": "Anata Male", - "portrait": "./assets/portraitImages/anata_male.png", - "manifest":"./character-assets/test.json", - "icon": "|", - "format": "vrm" + "icon": "./assets/icons/class-neural-hacker.svg", + "format": "vrm", + "collectionLock":"the-anata-nft", + "fullTraits":true } ], "loras":[ diff --git a/src/library/CharacterManifestData.js b/src/library/CharacterManifestData.js index 40796001..95734543 100644 --- a/src/library/CharacterManifestData.js +++ b/src/library/CharacterManifestData.js @@ -1,7 +1,7 @@ import { getAsArray } from "./utils"; export class CharacterManifestData{ - constructor(manifest){ + constructor(manifest, unlockedTraits = null){ const { assetsLocation, traitsDirectory, @@ -99,19 +99,19 @@ export class CharacterManifestData{ } defaultOptions(); - + // create texture and color traits first this.textureTraits = []; this.textureTraitsMap = null; - this.createTextureTraits(textureCollections); + this.createTextureTraits(textureCollections, false, unlockedTraits); this.colorTraits = []; this.colorTraitsMap = null; - this.createColorTraits(colorCollections); + this.createColorTraits(colorCollections, false, unlockedTraits); this.modelTraits = []; this.modelTraitsMap = null; - this.createModelTraits(traits); + this.createModelTraits(traits, false, unlockedTraits); } appendManifestData(manifestData, replaceExisting){ manifestData.textureTraits.forEach(newTextureTraitGroup => { @@ -361,11 +361,10 @@ export class CharacterManifestData{ // Given an array of traits, saves an array of TraitModels - createModelTraits(modelTraits, replaceExisting = false){ + createModelTraits(modelTraits, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.modelTraits = []; - getAsArray(modelTraits).forEach(traitObject => { - this.modelTraits.push(new TraitModelsGroup(this, traitObject)) + this.modelTraits.push(new TraitModelsGroup(this, traitObject, unlockedTraits)) }); this.modelTraitsMap = new Map(this.modelTraits.map(item => [item.trait, item])); @@ -382,21 +381,21 @@ export class CharacterManifestData{ }); } - createTextureTraits(textureTraits, replaceExisting = false){ + createTextureTraits(textureTraits, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.textureTraits = []; getAsArray(textureTraits).forEach(traitObject => { - this.textureTraits.push(new TraitTexturesGroup(this, traitObject)) + this.textureTraits.push(new TraitTexturesGroup(this, traitObject, unlockedTraits)) }); this.textureTraitsMap = new Map(this.textureTraits.map(item => [item.trait, item])); } - createColorTraits(colorTraits, replaceExisting = false){ + createColorTraits(colorTraits, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.colorTraits = []; getAsArray(colorTraits).forEach(traitObject => { - this.colorTraits.push(new TraitColorsGroup(this, traitObject)) + this.colorTraits.push(new TraitColorsGroup(this, traitObject, unlockedTraits)) }); this.colorTraitsMap = new Map(this.colorTraits.map(item => [item.trait, item])); @@ -407,7 +406,7 @@ export class CharacterManifestData{ // Must be created AFTER color collections and texture collections have been created class TraitModelsGroup{ - constructor(manifestData, options){ + constructor(manifestData, options, unlockedTraits = null){ const { trait, name, @@ -420,7 +419,7 @@ class TraitModelsGroup{ restrictedTypes = [] } = options; this.manifestData = manifestData; - + this.isRequired = manifestData.requiredTraits.indexOf(trait) !== -1; this.trait = trait; this.name = name; @@ -436,7 +435,14 @@ class TraitModelsGroup{ this.collection = []; this.collectionMap = null; - this.createCollection(collection); + + if (unlockedTraits == null){ + this.createCollection(collection); + } + else{ + this.createCollection(collection, false, unlockedTraits[trait] || []); + } + } appendCollection(modelTraitGroup, replaceExisting){ @@ -467,12 +473,21 @@ class TraitModelsGroup{ } } - createCollection(itemCollection, replaceExisting = false){ + createCollection(itemCollection, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.collection = []; - getAsArray(itemCollection).forEach(item => { - this.collection.push(new ModelTrait(this, item)) - }); + if (unlockedTraits == null){ + getAsArray(itemCollection).forEach(item => { + this.collection.push(new ModelTrait(this, item)) + }); + } + else{ + getAsArray(itemCollection).forEach(item => { + if (unlockedTraits.includes(item.id)){ + this.collection.push(new ModelTrait(this, item)) + } + }); + } this.collectionMap = new Map(this.collection.map(item => [item.id, item])); } @@ -506,7 +521,7 @@ class TraitModelsGroup{ } class TraitTexturesGroup{ - constructor(manifestData, options){ + constructor(manifestData, options, unlockedTraits = null){ const { trait, collection @@ -516,7 +531,13 @@ class TraitTexturesGroup{ this.collection = []; this.collectionMap = null; - this.createCollection(collection); + + if (unlockedTraits == null){ + this.createCollection(collection); + } + else{ + this.createCollection(collection, false, unlockedTraits[trait] || []); + } } @@ -543,12 +564,21 @@ class TraitTexturesGroup{ } }); } - createCollection(itemCollection, replaceExisting = false){ + createCollection(itemCollection, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.collection = []; - getAsArray(itemCollection).forEach(item => { - this.collection.push(new TextureTrait(this, item)) - }); + if (unlockedTraits == null){ + getAsArray(itemCollection).forEach(item => { + this.collection.push(new TextureTrait(this, item)) + }); + } + else{ + getAsArray(itemCollection).forEach(item => { + if (unlockedTraits.includes(item.id)){ + this.collection.push(new TextureTrait(this, item)) + } + }); + } this.collectionMap = new Map(this.collection.map(item => [item.id, item])); } @@ -567,7 +597,7 @@ class TraitTexturesGroup{ } } class TraitColorsGroup{ - constructor(manifestData, options){ + constructor(manifestData, options, unlockedTraits = null){ const { trait, collection @@ -577,7 +607,13 @@ class TraitColorsGroup{ this.collection = []; this.collectionMap = null; - this.createCollection(collection); + + if (unlockedTraits == null){ + this.createCollection(collection); + } + else{ + this.createCollection(collection, false, unlockedTraits[trait] || []); + } } appendCollection(colorTraitGroup, replaceExisting){ @@ -602,12 +638,21 @@ class TraitColorsGroup{ } }); } - createCollection(itemCollection, replaceExisting = false){ + createCollection(itemCollection, replaceExisting = false, unlockedTraits = null){ if (replaceExisting) this.collection = []; - getAsArray(itemCollection).forEach(item => { - this.collection.push(new ColorTrait(this, item)) - }); + if (unlockedTraits == null){ + getAsArray(itemCollection).forEach(item => { + this.collection.push(new ColorTrait(this, item)) + }); + } + else{ + getAsArray(itemCollection).forEach(item => { + if (unlockedTraits.includes(item.id)){ + this.collection.push(new ColorTrait(this, item)) + } + }); + } this.collectionMap = new Map(this.collection.map(item => [item.id, item])); } diff --git a/src/library/characterManager.js b/src/library/characterManager.js index 045afbaa..22e10417 100644 --- a/src/library/characterManager.js +++ b/src/library/characterManager.js @@ -702,10 +702,11 @@ export class CharacterManager { * Sets an existing manifest data for the character. * * @param {object} manifest - The loaded mmanifest object. + * @param {Array} unlockedTraits - Optional string array of the traits that will be unlocked, if none set, all traits will be unlocked. * @returns {Promise} A Promise that resolves when the manifest is successfully loaded, * or rejects with an error message if loading fails. */ - setManifest(manifest){ + setManifest(manifest, unlockedTraits = null){ this.removeCurrentCharacter(); return new Promise(async (resolve, reject) => { try{ @@ -713,7 +714,7 @@ export class CharacterManager { this.manifest = manifest; if (this.manifest) { // Create a CharacterManifestData instance based on the fetched manifest - this.manifestData = new CharacterManifestData(this.manifest); + this.manifestData = new CharacterManifestData(this.manifest, unlockedTraits); // If an animation manager is available, set it up if (this.animationManager) { @@ -772,17 +773,18 @@ export class CharacterManager { * Loads the manifest data for the character. * * @param {string} url - The URL of the manifest. + * @param {Array} unlockedTraits - Optional string array of the traits that will be unlocked, if none set, all traits will be unlocked. * @returns {Promise} A Promise that resolves when the manifest is successfully loaded, * or rejects with an error message if loading fails. */ - loadManifest(url) { + loadManifest(url, unlockedTraits= null) { // remove in case character was loaded return new Promise(async (resolve, reject) => { try { // Fetch the manifest data asynchronously const manifest = await this._fetchManifest(url); - this.setManifest(manifest).then(()=>{ + this.setManifest(manifest, unlockedTraits).then(()=>{ resolve(); }) diff --git a/src/library/mint-utils.js b/src/library/mint-utils.js index e29db9f7..9b35c9bc 100644 --- a/src/library/mint-utils.js +++ b/src/library/mint-utils.js @@ -35,10 +35,9 @@ export function getOpenseaCollection(address, collection) { method: 'GET', headers: { accept: 'application/json', 'x-api-key': opensea_Key }, }; - console.log(options); // Returning a Promise return new Promise((resolve, reject) => { - fetch('https://api.opensea.io/api/v2/chain/ethereum/account/' + address + '/nfts?collection=' + collection, options) + fetch('https://api.opensea.io/api/v2/chain/ethereum/account/' + address + '/nfts?limit=200&collection=' + collection, options) .then(response => { // Check if the response status is ok (2xx range) if (response.ok) { @@ -59,9 +58,50 @@ export function getOpenseaCollection(address, collection) { }); } +export function ownsCollection(address, collection){ + const options = { + method: 'GET', + headers: { accept: 'application/json', 'x-api-key': opensea_Key }, + }; + // Returning a Promise + return new Promise((resolve, reject) => { + fetch('https://api.opensea.io/api/v2/chain/ethereum/account/' + address + '/nfts?limit=1&collection=' + collection, options) + .then(response => { + // Check if the response status is ok (2xx range) + if (response.ok) { + return response.json(); + } else { + // If the response status is not ok, reject the Promise with an error message + reject('Failed to fetch data from Opensea API'); + } + }) + .then(response => { + // Resolve the Promise with the JSON response + resolve(response.nfts.length>0); + }) + .catch(err => { + // Reject the Promise with the error encountered during the fetch + reject(err); + }); + }); +} + +export async function currentWallet(){ + console.log("get") + const chain = await window.ethereum.request({ method: 'eth_chainId' }) + if (parseInt(chain, 16) == parseInt(chainId, 16)) { + const addressArray = await window.ethereum.request({ + method: 'eth_requestAccounts', + }) + console.log(addressArray); + return addressArray.length > 0 ? addressArray[0] : "" + } + return ""; +} // ready to test export async function connectWallet(){ + console.log("connect") if (window.ethereum) { try { const chain = await window.ethereum.request({ method: 'eth_chainId' }) @@ -70,6 +110,7 @@ export async function connectWallet(){ const addressArray = await window.ethereum.request({ method: 'eth_requestAccounts', }) + console.log(addressArray); return addressArray.length > 0 ? addressArray[0] : "" } else { try { diff --git a/src/pages/Create.jsx b/src/pages/Create.jsx index e96ba901..207150d8 100644 --- a/src/pages/Create.jsx +++ b/src/pages/Create.jsx @@ -9,6 +9,12 @@ import { SceneContext } from "../context/SceneContext" import { SoundContext } from "../context/SoundContext" import { AudioContext } from "../context/AudioContext" +import { local } from "../library/store" + +import { connectWallet, getOpenseaCollection, ownsCollection, currentWallet } from "../library/mint-utils" + +import { getAsArray } from "../library/utils" + function Create() { // Translate hook @@ -19,10 +25,40 @@ function Create() { const { isMute } = React.useContext(AudioContext) const { manifest, characterManager } = React.useContext(SceneContext) const [ classes, setClasses ] = useState([]) + const [ collections, setCollections ] = useState([]) + const [ currentAddress, setCurrentAddress] = useState(""); + const [ ownedCollections, setOwnedCollections] = useState(null); + const [ enabledRefresh, setEnabledRefresh] = useState(true); + let loaded = false + let [isLoaded, setIsLoaded] = useState(false) + let [requireWalletConnect, setRequireWalletConnect] = useState(false) + + useEffect(()=>{ + if (requireWalletConnect == true){ + if (loaded || isLoaded) return + setIsLoaded(true) + loaded = true; + if (currentAddress == ""){ + const getWallet = async ()=>{ + const wallet = await currentWallet(); + setCurrentAddress(wallet); + if (wallet != ""){ + await fetchWalletNFTS(); + } + } + getWallet(); + } + } + },[requireWalletConnect]) useEffect(() => { if (manifest?.characters != null){ + let requiresConnect = false; const manifestClasses = manifest.characters.map((c) => { + let enabled = c.collectionLock == null ? false : true; + if (c.collectionLock != null) + requiresConnect = true; + console.log(c.collectionLock) return { name:c.name, image:c.portrait, @@ -30,10 +66,24 @@ function Create() { manifest: c.manifest, icon:c.icon, format:c.format, - disabled:false + disabled:enabled, + collection: getAsArray(c.collectionLock), + fullTraits: c.fullTraits || false } }) + + const nonRepeatingCollections = []; + const seenCollections = new Set(); + manifest.characters.forEach((c) => { + if (c.collectionLock != null && !seenCollections.has(c.collectionLock)) { + nonRepeatingCollections.push(c.collectionLock); + seenCollections.add(c.collectionLock); + } + }); + setCollections(nonRepeatingCollections); setClasses(manifestClasses); + setRequireWalletConnect(requiresConnect); + } }, [manifest]) @@ -44,8 +94,62 @@ function Create() { const selectClass = async (index) => { setIsLoading(true) + console.log(classes[index]); // Load manifest first - characterManager.loadManifest(manifest.characters[index].manifest).then(()=>{ + let unlockedTraits = null; + if (classes[index].collection.length > 0 && classes[index].fullTraits == false){ + console.log("got 1") + const address = await connectWallet(); + const result = await getOpenseaCollection(address,classes[index].collection[0]) + const nfts = getAsArray(result?.nfts); + console.log("nfts", result?.nfts); + const nftsMeta = []; + + const promises = nfts.map(nft => + new Promise((resolve)=>{ + fetch(nft.metadata_url) + .then(response=>{ + response.json() + .then(metadata=>{ + nftsMeta.push(metadata); + resolve (); + }) + .catch(err=>{ + console.warn("error converting to json"); + console.error(err); + resolve () + }) + }) + .catch(err=>{ + // resolve even if it fails, to avoid complete freeze + console.warn("error getting " + nft.metadata_url + ", skpping") + console.error(err); + resolve () + }) + }) + ); + + await Promise.all(promises); + + unlockedTraits = {}; + const getTraitsFromNFTsArray = (arr) =>{ + const nftArr = getAsArray(arr); + nftArr.forEach(nft => { + nft.attributes.forEach(attr => { + if (unlockedTraits[attr.trait_type] == null) + unlockedTraits[attr.trait_type] = [] + if (!unlockedTraits[attr.trait_type].includes(attr.value)) + unlockedTraits[attr.trait_type].push(attr.value); + }); + + }); + } + getTraitsFromNFTsArray(nftsMeta); + + console.log(unlockedTraits) + // unlockedTraits + } + characterManager.loadManifest(manifest.characters[index].manifest, unlockedTraits).then(()=>{ setViewMode(ViewMode.APPEARANCE) // When Manifest is Loaded, load initial traits from given manifest characterManager.loadInitialTraits().then(()=>{ @@ -55,7 +159,77 @@ function Create() { !isMute && playSound('classSelect'); } - const hoverClass = () => { + useEffect(()=>{ + if (ownedCollections != null){ + + const editedClasses = classes.map((c) => { + let locked = c.collection.length > 0 ? true : false; + for (let i =0; i < c.collection.length;i++){ + const collection = c.collection[i]; + if (ownedCollections[collection] == true ){ + locked = false; + break; + } + } + return { + name:c.name, + image:c.image, + description: c.description, + manifest: c.manifest, + icon:c.icon, + format:c.format, + disabled:locked, + collection: c.collection, + fullTraits: c.fullTraits + }}); + + console.log(editedClasses) + setClasses(editedClasses); + } + }, [ownedCollections]) + + const fetchWalletNFTS = async(getLocal = true)=>{ + const address = await connectWallet() + if (address != ""){ + //console.log(local[address + "collections"]); + if (getLocal && local[address + "collections"] != null){ + setOwnedCollections(local[address + "collections"]); + } + else{ + // get it from opensea + console.log("from opensea") + setEnabledRefresh(false) + const owned = {}; + const promises = collections.map(collection => + ownsCollection(address, collection).then(result => { + owned[collection] = result; + }) + ); + + await Promise.all(promises); + + local[address + "collections"] = owned; + + setOwnedCollections(owned); + + setTimeout(() => { + setEnabledRefresh(true); + }, 5000); + } + + + // getAllCollections(address).then((result)=>{ + // // setWalletNFTs(result.nfts); + // console.log(result); + // }) + // getOpenseaCollection(address,'the-anata-nft').then((result)=>{ + // // setWalletNFTs(result.nfts); + // console.log(result.nfts); + // }) + } + } + + const hoverSound = () => { !isMute && playSound('classMouseOver'); } @@ -64,6 +238,8 @@ function Create() {
{t('pageTitles.chooseClass')}
+ +
@@ -78,13 +254,13 @@ function Create() { } onClick={ characterClass["disabled"] - ? null + ? () => fetchWalletNFTS() : () => selectClass(i) } onMouseOver={ characterClass["disabled"] - ? null - : () => hoverClass() + ? () => hoverSound() + : () => hoverSound() } >
+
+
fetchWalletNFTS(false): + ()=>{} + } + onMouseOver={ + () => hoverSound() + }/> +
+
{ - {opensea_Key && opensea_Key != "" && + { + // opensea_Key && opensea_Key != "" && } {/*