diff --git a/README.md b/README.md index fc03146..c53eb55 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Decentraland collections graph -- Mainnet: https://thegraph.com/explorer/subgraph/decentraland/collections-ethereum-mainnet (QmSRex2cvskLzaMpjeeZounbtFcMwirN7vaCf5LMo6FFna) +- Mainnet: https://thegraph.com/explorer/subgraph/decentraland/collections-ethereum-mainnet (QmdWuDd5S8TfhEV9SAXTpGi2pfPQGu8kJfs6mVB787fy4A) - Ropsten: https://thegraph.com/explorer/subgraph/decentraland/collections-ethereum-ropsten (QmZTJrrSmAjKm1Vq8HRRkUu7FxvAbPXdum8vYmmR3pEF6w) -- Matic: https://thegraph.com/explorer/subgraph/decentraland/collections-matic-mainnet (QmcvFsuwJMC4o2B3Xp7kejU4YSDjFDNE89JwvgpgyGjmK8) -- Mumbai: https://thegraph.com/explorer/subgraph/decentraland/collections-matic-mumbai (QmSgPsD7sPmYNXysuFtjtXnENMRsHhAn5bZFaTfpVKDUAt) +- Matic: https://thegraph.com/explorer/subgraph/decentraland/collections-matic-mainnet (QmeajVAfQe1coz6H6SnR9pGhBo1Ntg1pMUAR7o5bxALjHK) +- Mumbai: https://thegraph.com/explorer/subgraph/decentraland/collections-matic-mumbai (QmSXYRaYd6Tmufanyw7URhLST3x7VAJ9rPaCHx2Lmgvb4f) ### Install diff --git a/schema.graphql b/schema.graphql index d66dbed..2608c0a 100644 --- a/schema.graphql +++ b/schema.graphql @@ -68,6 +68,11 @@ type Item @entity { searchWearableCategory: WearableCategory searchWearableRarity: String # We're using String instead of WearableRarity here so we can later query this field via ()_in searchWearableBodyShapes: [WearableBodyShape!] + + ## Emote search fields + searchEmoteCategory: EmoteCategory + searchEmoteRarity: String # We're using String instead of WearableRarity here so we can later query this field via ()_in + searchEmoteBodyShapes: [WearableBodyShape!] } type NFT @entity { @@ -109,6 +114,11 @@ type NFT @entity { searchWearableRarity: String # We're using String instead of WearableRarity here so we can later query this field via ()_in searchWearableBodyShapes: [WearableBodyShape!] + ## Emote search fields + searchEmoteCategory: EmoteCategory + searchEmoteRarity: String # We're using String instead of WearableRarity here so we can later query this field via ()_in + searchEmoteBodyShapes: [WearableBodyShape!] + ## Order search fields searchOrderStatus: OrderStatus searchOrderPrice: BigInt @@ -120,6 +130,7 @@ type Metadata @entity { id: ID! itemType: ItemType! wearable: Wearable + emote: Emote } enum ItemType @entity { @@ -127,6 +138,7 @@ enum ItemType @entity { wearable_v1 wearable_v2 smart_wearable_v1 + emote_v1 } type Wearable @entity { @@ -139,6 +151,16 @@ type Wearable @entity { bodyShapes: [WearableBodyShape!] } +type Emote @entity { + id: ID! + name: String! + description: String! + collection: String! + category: EmoteCategory! + rarity: WearableRarity! + bodyShapes: [WearableBodyShape!] +} + enum WearableCategory @entity { eyebrows eyes @@ -158,6 +180,11 @@ enum WearableCategory @entity { skin } +enum EmoteCategory @entity { + simple + loop +} + enum WearableRarity @entity { common uncommon diff --git a/src/entities/schema.ts b/src/entities/schema.ts index ca9b774..090c9de 100644 --- a/src/entities/schema.ts +++ b/src/entities/schema.ts @@ -663,6 +663,60 @@ export class Item extends Entity { ); } } + + get searchEmoteCategory(): string | null { + let value = this.get("searchEmoteCategory"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toString(); + } + } + + set searchEmoteCategory(value: string | null) { + if (value === null) { + this.unset("searchEmoteCategory"); + } else { + this.set("searchEmoteCategory", Value.fromString(value as string)); + } + } + + get searchEmoteRarity(): string | null { + let value = this.get("searchEmoteRarity"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toString(); + } + } + + set searchEmoteRarity(value: string | null) { + if (value === null) { + this.unset("searchEmoteRarity"); + } else { + this.set("searchEmoteRarity", Value.fromString(value as string)); + } + } + + get searchEmoteBodyShapes(): Array | null { + let value = this.get("searchEmoteBodyShapes"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toStringArray(); + } + } + + set searchEmoteBodyShapes(value: Array | null) { + if (value === null) { + this.unset("searchEmoteBodyShapes"); + } else { + this.set( + "searchEmoteBodyShapes", + Value.fromStringArray(value as Array) + ); + } + } } export class NFT extends Entity { @@ -1061,6 +1115,60 @@ export class NFT extends Entity { } } + get searchEmoteCategory(): string | null { + let value = this.get("searchEmoteCategory"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toString(); + } + } + + set searchEmoteCategory(value: string | null) { + if (value === null) { + this.unset("searchEmoteCategory"); + } else { + this.set("searchEmoteCategory", Value.fromString(value as string)); + } + } + + get searchEmoteRarity(): string | null { + let value = this.get("searchEmoteRarity"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toString(); + } + } + + set searchEmoteRarity(value: string | null) { + if (value === null) { + this.unset("searchEmoteRarity"); + } else { + this.set("searchEmoteRarity", Value.fromString(value as string)); + } + } + + get searchEmoteBodyShapes(): Array | null { + let value = this.get("searchEmoteBodyShapes"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toStringArray(); + } + } + + set searchEmoteBodyShapes(value: Array | null) { + if (value === null) { + this.unset("searchEmoteBodyShapes"); + } else { + this.set( + "searchEmoteBodyShapes", + Value.fromStringArray(value as Array) + ); + } + } + get searchOrderStatus(): string | null { let value = this.get("searchOrderStatus"); if (value === null || value.kind == ValueKind.NULL) { @@ -1185,6 +1293,23 @@ export class Metadata extends Entity { this.set("wearable", Value.fromString(value as string)); } } + + get emote(): string | null { + let value = this.get("emote"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toString(); + } + } + + set emote(value: string | null) { + if (value === null) { + this.unset("emote"); + } else { + this.set("emote", Value.fromString(value as string)); + } + } } export class Wearable extends Entity { @@ -1280,6 +1405,99 @@ export class Wearable extends Entity { } } +export class Emote extends Entity { + constructor(id: string) { + super(); + this.set("id", Value.fromString(id)); + } + + save(): void { + let id = this.get("id"); + assert(id !== null, "Cannot save Emote entity without an ID"); + assert( + id.kind == ValueKind.STRING, + "Cannot save Emote entity with non-string ID. " + + 'Considering using .toHex() to convert the "id" to a string.' + ); + store.set("Emote", id.toString(), this); + } + + static load(id: string): Emote | null { + return store.get("Emote", id) as Emote | null; + } + + get id(): string { + let value = this.get("id"); + return value.toString(); + } + + set id(value: string) { + this.set("id", Value.fromString(value)); + } + + get name(): string { + let value = this.get("name"); + return value.toString(); + } + + set name(value: string) { + this.set("name", Value.fromString(value)); + } + + get description(): string { + let value = this.get("description"); + return value.toString(); + } + + set description(value: string) { + this.set("description", Value.fromString(value)); + } + + get collection(): string { + let value = this.get("collection"); + return value.toString(); + } + + set collection(value: string) { + this.set("collection", Value.fromString(value)); + } + + get category(): string { + let value = this.get("category"); + return value.toString(); + } + + set category(value: string) { + this.set("category", Value.fromString(value)); + } + + get rarity(): string { + let value = this.get("rarity"); + return value.toString(); + } + + set rarity(value: string) { + this.set("rarity", Value.fromString(value)); + } + + get bodyShapes(): Array | null { + let value = this.get("bodyShapes"); + if (value === null || value.kind == ValueKind.NULL) { + return null; + } else { + return value.toStringArray(); + } + } + + set bodyShapes(value: Array | null) { + if (value === null) { + this.unset("bodyShapes"); + } else { + this.set("bodyShapes", Value.fromStringArray(value as Array)); + } + } +} + export class Rarity extends Entity { constructor(id: string) { super(); diff --git a/src/modules/metadata/emote/categories.ts b/src/modules/metadata/emote/categories.ts new file mode 100644 index 0000000..934d638 --- /dev/null +++ b/src/modules/metadata/emote/categories.ts @@ -0,0 +1,3 @@ +// Emote categories +export const SIMPLE = 'simple' +export const LOOP = 'loop' diff --git a/src/modules/metadata/emote/index.ts b/src/modules/metadata/emote/index.ts new file mode 100644 index 0000000..27b88c4 --- /dev/null +++ b/src/modules/metadata/emote/index.ts @@ -0,0 +1,69 @@ +import { log } from '@graphprotocol/graph-ts' +import { isValidBodyShape } from '..' +import { Emote, Item, Metadata, NFT, Wearable } from '../../../entities/schema' +import { toLowerCase } from '../../../utils' +import { LOOP, SIMPLE } from './categories' + +/** + * @dev The item's rawMetadata for emotes should follow: version:item_type:name:description:category:bodyshapes + * @param item + */ +export function buildEmoteItem(item: Item): Emote | null { + let id = item.id + let data = item.rawMetadata.split(':') + if ((data.length == 6 || data.length == 8) && isValidEmoteCategory(data[4]) && isValidBodyShape(data[5].split(','))) { + let emote = Emote.load(id) + + if (emote == null) { + emote = new Emote(id) + } + + emote.collection = item.collection + emote.name = data[2] + emote.description = data[3] + emote.rarity = item.rarity + emote.category = data[4] + emote.bodyShapes = data[5].split(',') // Could be more than one + emote.save() + + return emote + } + + return null +} + +function isValidEmoteCategory(category: string): boolean { + if (category == SIMPLE || category == LOOP) { + return true + } + + log.error('Invalid Category {}', [category]) + + return false +} + +export function setItemEmoteSearchFields(item: Item): Item { + let metadata = Metadata.load(item.metadata) + let emote = Emote.load(metadata.emote) + + item.searchText = toLowerCase(emote.name + ' ' + emote.description) + item.searchItemType = item.itemType + item.searchEmoteCategory = emote.category + item.searchEmoteBodyShapes = emote.bodyShapes + item.searchEmoteRarity = emote.rarity + + return item +} + +export function setNFTEmoteSearchFields(nft: NFT): NFT { + let metadata = Metadata.load(nft.metadata) + let emote = Emote.load(metadata.emote) + + nft.searchText = toLowerCase(emote.name + ' ' + emote.description) + nft.searchItemType = nft.itemType + nft.searchEmoteCategory = emote.category + nft.searchEmoteBodyShapes = emote.bodyShapes + nft.searchEmoteRarity = emote.rarity + + return nft +} diff --git a/src/modules/metadata/index.ts b/src/modules/metadata/index.ts index 8d5c558..b1d139c 100644 --- a/src/modules/metadata/index.ts +++ b/src/modules/metadata/index.ts @@ -1,12 +1,9 @@ import * as itemTypes from './itemTypes' import { Item, NFT, Metadata } from '../../entities/schema' -import { - setNFTWearableSearchFields, - setItemWearableSearchFields, - buildWearableItem, - buildWearableV1 -} from './wearable' +import { setNFTWearableSearchFields, setItemWearableSearchFields, buildWearableItem, buildWearableV1 } from './wearable' import { Wearable as WearableRepresentation } from '../../data/wearablesV1' +import { buildEmoteItem, setItemEmoteSearchFields, setNFTEmoteSearchFields } from './emote' +import { log } from '@graphprotocol/graph-ts' /** * @notice the item's metadata must follow: version:item_type:representation_id:data @@ -23,13 +20,30 @@ export function buildItemMetadata(item: Item): Metadata { if (data.length >= 2) { let type = data[1] - let wearable = buildWearableItem(item) - if (wearable != null && type == itemTypes.WEARABLE_TYPE_SHORT) { - metadata.itemType = itemTypes.WEARABLE_V2 - metadata.wearable = wearable.id - } else if (wearable != null && type == itemTypes.SMART_WEARABLE_TYPE_SHORT) { - metadata.itemType = itemTypes.SMART_WEARABLE_V1 - metadata.wearable = wearable.id + if (type == itemTypes.WEARABLE_TYPE_SHORT) { + let wearable = buildWearableItem(item) + if (wearable != null) { + metadata.itemType = itemTypes.WEARABLE_V2 + metadata.wearable = wearable.id + } else { + metadata.itemType = itemTypes.UNDEFINED + } + } else if (type == itemTypes.SMART_WEARABLE_TYPE_SHORT) { + let wearable = buildWearableItem(item) + if (wearable != null) { + metadata.itemType = itemTypes.SMART_WEARABLE_V1 + metadata.wearable = wearable.id + } else { + metadata.itemType = itemTypes.UNDEFINED + } + } else if (type == itemTypes.EMOTE_TYPE_SHORT) { + let emote = buildEmoteItem(item) + if (emote != null) { + metadata.itemType = itemTypes.EMOTE_V1 + metadata.emote = emote.id + } else { + metadata.itemType = itemTypes.UNDEFINED + } } else { metadata.itemType = itemTypes.UNDEFINED } @@ -42,7 +56,6 @@ export function buildItemMetadata(item: Item): Metadata { return metadata! } - export function buildWearableV1Metadata(item: Item, representation: WearableRepresentation): Metadata { let metadata = new Metadata(representation.id) @@ -56,27 +69,37 @@ export function buildWearableV1Metadata(item: Item, representation: WearableRepr return metadata! } - export function setItemSearchFields(item: Item): Item { - if ( - item.itemType == itemTypes.WEARABLE_V2 || - item.itemType == itemTypes.WEARABLE_V1 || - item.itemType == itemTypes.SMART_WEARABLE_V1 - ) { + if (item.itemType == itemTypes.WEARABLE_V2 || item.itemType == itemTypes.WEARABLE_V1 || item.itemType == itemTypes.SMART_WEARABLE_V1) { return setItemWearableSearchFields(item) } + if (item.itemType == itemTypes.EMOTE_V1) { + return setItemEmoteSearchFields(item) + } return item } export function setNFTSearchFields(nft: NFT): NFT { - if ( - nft.itemType == itemTypes.WEARABLE_V2 || - nft.itemType == itemTypes.WEARABLE_V1 || - nft.itemType == itemTypes.SMART_WEARABLE_V1 - ) { + if (nft.itemType == itemTypes.WEARABLE_V2 || nft.itemType == itemTypes.WEARABLE_V1 || nft.itemType == itemTypes.SMART_WEARABLE_V1) { return setNFTWearableSearchFields(nft) } + if (nft.itemType == itemTypes.EMOTE_V1) { + return setNFTEmoteSearchFields(nft) + } + return nft } + +export function isValidBodyShape(bodyShapes: string[]): boolean { + for (let i = 0; i++; i < bodyShapes.length) { + let bodyShape = bodyShapes[i] + if (bodyShape != 'BaseFemale' && bodyShape != 'BaseMale') { + log.error('Invalid BodyShape {}', [bodyShape]) + return false + } + } + + return true +} diff --git a/src/modules/metadata/itemTypes.ts b/src/modules/metadata/itemTypes.ts index a920518..627df9e 100644 --- a/src/modules/metadata/itemTypes.ts +++ b/src/modules/metadata/itemTypes.ts @@ -5,5 +5,8 @@ export const WEARABLE_V2 = 'wearable_v2' export const SMART_WEARABLE_V1 = 'smart_wearable_v1' +export const EMOTE_V1 = 'emote_v1' + export const WEARABLE_TYPE_SHORT = 'w' export const SMART_WEARABLE_TYPE_SHORT = 'sw' +export const EMOTE_TYPE_SHORT = 'e' diff --git a/src/modules/metadata/wearable/index.ts b/src/modules/metadata/wearable/index.ts index 5144326..056b4bc 100644 --- a/src/modules/metadata/wearable/index.ts +++ b/src/modules/metadata/wearable/index.ts @@ -1,7 +1,7 @@ import { log } from '@graphprotocol/graph-ts' import * as categories from './categories' -import { Collection, Item, NFT, Metadata, Wearable } from '../../../entities/schema' +import { Collection, Item, NFT, Metadata, Wearable, Emote } from '../../../entities/schema' import { Wearable as WearableRepresentation, binance_us_collection, @@ -50,6 +50,7 @@ import { } from '../../../data/wearablesV1' import { getNetwork } from '../../network' import { toLowerCase } from '../../../utils' +import { isValidBodyShape } from '..' /** * @dev The item's rawMetadata for wearables should follow: version:item_type:name:description:category:bodyshapes @@ -59,7 +60,7 @@ import { toLowerCase } from '../../../utils' export function buildWearableItem(item: Item): Wearable | null { let id = item.id let data = item.rawMetadata.split(':') - if ((data.length == 6 || data.length == 8) && isValidCategory(data[4]) && isValidBodyShape(data[5].split(','))) { + if ((data.length == 6 || data.length == 8) && isValidWearableCategory(data[4]) && isValidBodyShape(data[5].split(','))) { let wearable = Wearable.load(id) if (wearable == null) { @@ -80,7 +81,7 @@ export function buildWearableItem(item: Item): Wearable | null { return null } -function isValidCategory(category: string): boolean { +function isValidWearableCategory(category: string): boolean { if ( category == 'eyebrows' || category == 'eyes' || @@ -107,18 +108,6 @@ function isValidCategory(category: string): boolean { return false } -function isValidBodyShape(bodyShapes: string[]): boolean { - for (let i = 0; i++; i < bodyShapes.length) { - let bodyShape = bodyShapes[i] - if (bodyShape != 'BaseFemale' && bodyShape != 'BaseMale') { - log.error('Invalid BodyShape {}', [bodyShape]) - return false - } - } - - return true -} - export function buildWearableV1(item: Item, representation: WearableRepresentation): Wearable { let wearable = new Wearable(representation.id)