From 649dadb7878cb2949d2a00aafcb563757eb30d12 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Thu, 1 Apr 2021 17:13:50 -0300 Subject: [PATCH 1/4] First commit --- src/avatar/index.ts | 87 +++++++++++++++++++++++++------------------ src/shared/utils.ts | 43 +++++++++++++++++++++ src/wearable/index.ts | 37 ++++++++++++++++-- 3 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 src/shared/utils.ts diff --git a/src/avatar/index.ts b/src/avatar/index.ts index c7f7c52..a75ba98 100644 --- a/src/avatar/index.ts +++ b/src/avatar/index.ts @@ -2,7 +2,8 @@ import { getUserAccount } from '@decentraland/EthereumController' import { getCurrentRealm } from '@decentraland/EnvironmentAPI' import * as eth from 'eth-connect' import { Profiles, Snapshots } from './types' -import { Rarity, rarityLevel, Wearable } from '../wearable/types' +import { Rarity, rarityLevel, Representation, Type, Wearable } from '../wearable/types' +import { getCatalystUrl, mapV2WearableIntoV1 } from '../shared/utils' /** * Returns profile of an address @@ -12,9 +13,7 @@ import { Rarity, rarityLevel, Wearable } from '../wearable/types' export async function getUserInfo(address?: eth.Address) { const realm = address ? 'https://peer.decentraland.org' - : await getCurrentRealm().then((r: any) => - r.domain != 'http://127.0.0.1:8000' ? r.domain : 'https://peer.decentraland.org' - ) + : await getCatalystUrl() if (!address) address = await getUserAccount().then((a) => a.toLowerCase()) return (await fetch(`${realm}/content/entities/profiles?pointer=${address?.toLowerCase()}`) .then((res) => res.json()) @@ -28,12 +27,18 @@ export async function getUserInfo(address?: eth.Address) { */ export async function getUserInventory(address?: eth.Address) { if (!address) address = await getUserAccount() - const response = await fetch(`https://wearable-api.decentraland.org/v2/addresses/${address}/wearables?fields=id`) - const inventory: { id: string }[] = await response.json() - return inventory.map((wearable) => wearable.id) + const catalystUrl = await getCatalystUrl() + const response = await fetch(`${catalystUrl}/lambdas/collections/wearables-by-owner/${address}`) + const inventory: { urn: string, amount: number }[] = await response.json() + const result: string[] = [] + for (const { urn, amount } of inventory) { + for (let i = 0; i < amount; i++) { + result.push(urn) + } + } + return result } - /** * Returns wearables inventory of an address with full data on each wearable * @@ -41,12 +46,18 @@ export async function getUserInventory(address?: eth.Address) { */ export async function getUserFullInventory(address?: eth.Address) { if (!address) address = await getUserAccount() - const response = await fetch( - `https://wearable-api.decentraland.org/v2/addresses/${address}/wearables` - ) - const inventory: Wearable[] = await response.json() - return inventory + const catalystUrl = await getCatalystUrl() + const response = await fetch(`${catalystUrl}/lambdas/collections/wearables-by-owner/${address}?includeDefinitions`) + const inventory: { amount: number, definition: any }[] = await response.json() + const result: Wearable[] = [] + for (const { definition, amount } of inventory) { + const mapped = mapV2WearableIntoV1(catalystUrl, definition) + for (let i = 0; i < amount; i++) { + result.push(mapped) + } } + return result +} /** * Returns boolean if the user has an item in their inventory or equiped @@ -54,19 +65,8 @@ export async function getUserInventory(address?: eth.Address) { * @param wearable DCL name of the wearable ('dcl://dcl_launch/razor_blade_upper_body') * @param equiped true if currently wearing */ -export async function itemInInventory(wearable: string, equiped: boolean = false) { - const profile = await getUserInfo() - if (equiped) { - for (const item of profile.metadata.avatars[0]?.avatar.wearables) { - if (item == wearable) return true - } - } else { - const inventory = await getUserInventory() - for (const item of inventory) { - if (item == wearable) return true - } - } - return false +export function itemInInventory(wearable: string, equiped: boolean = false) { + return itemsInInventory([wearable], equiped) } /** @@ -76,15 +76,17 @@ export async function itemInInventory(wearable: string, equiped: boolean = false * @param equiped true if currently wearing */ export async function itemsInInventory(wearables: string[], equiped: boolean = false) { - const profile = await getUserInfo() + const wearablesAsUrn = wearables.map(mapToUrn) if (equiped) { - for (const item of profile.metadata.avatars[0]?.avatar.wearables) { - if (wearables.indexOf(item) != -1) return true + const equiped = await equipedItems() + const equipedAsUrn = equiped.map(mapToUrn) + for (const item of equipedAsUrn) { + if (wearablesAsUrn.indexOf(item) != -1) return true } } else { const inventory = await getUserInventory() for (const item of inventory) { - if (wearables.indexOf(item) != -1) return true + if (wearablesAsUrn.indexOf(item) != -1) return true } } return false @@ -112,7 +114,7 @@ let rarestEquippedItem: rarityLevel = 0 const profile = await getUserInfo() const inventory = await getUserFullInventory() if (!profile || !inventory) return rarityLevel.none - + if (equiped) { for (const item of profile.metadata.avatars[0]?.avatar.wearables) { for (let invItem of inventory) { @@ -131,7 +133,7 @@ let rarestEquippedItem: rarityLevel = 0 log(rarityLevel[rarestEquippedItem]) return rarestEquippedItem } - + export function updateRarity(rarity: Rarity) { let rarityNum: number = 0 switch (rarity) { @@ -162,7 +164,7 @@ let rarestEquippedItem: rarityLevel = 0 //log('new Rarest ', rarestEquippedItem, ' ') } } - + /** * Returns a Snapshots object, containing URLs to various snapshots of a player's face and full body @@ -174,11 +176,22 @@ export async function getPlayerSnapshots(playerId?: string) :Promise - r.domain != 'http://127.0.0.1:8000' ? r.domain : 'https://peer.decentraland.org' - ) + const realm = await getCatalystUrl() return (await fetch(`${realm}/lambdas/profiles?field=snapshots&id=${playerId.toLowerCase()}`) .then((res) => res.json()) .then((res) => (res[0].avatars.length ? res[0].avatars[0].avatar.snapshots as Snapshots : null))) - } \ No newline at end of file + } + +function mapToUrn(wearableId: string) { + if (wearableId.indexOf('dcl://') < 0) { + // Already urn + return wearableId + } + const [collectionName, wearableName ] = wearableId.replace('dcl://', '').split('/') + if (collectionName === 'base-avatars') { + return `urn:decentraland:off-chain:base-avatars:${wearableName}` + } else { + return `urn:decentraland:ethereum:collections-v1:${collectionName}:${wearableName}` + } +} \ No newline at end of file diff --git a/src/shared/utils.ts b/src/shared/utils.ts new file mode 100644 index 0000000..4e2b4d4 --- /dev/null +++ b/src/shared/utils.ts @@ -0,0 +1,43 @@ +import { getCurrentRealm } from "@decentraland/EnvironmentAPI" +import { Representation, Type, Wearable } from "../wearable/types" + +export function getCatalystUrl(): Promise { + return getCurrentRealm() + .then((r: any) => r.domain != 'http://127.0.0.1:8000' ? r.domain : 'https://peer.decentraland.org') +} + +export function mapV2WearableIntoV1(catalystUrl: string, v2Wearable: any): Wearable { + const { id, data, rarity, i18n, thumbnail, image } = v2Wearable + const { category, tags, hides, replaces, representations } = data + const newRepresentations: Representation[] = representations.map(mapV2RepresentationIntoV1) + const newThumbnail = thumbnail.substring(thumbnail.lastIndexOf('/') + 1) + const newImage = image ? image.substring(image.lastIndexOf('/') + 1) : undefined + + return { + id, + type: Type.Wearable, + category, + tags, + hides, + replaces, + rarity, + representations: newRepresentations, + i18n, + thumbnail: newThumbnail, + image: newImage, + baseUrl: `${catalystUrl}/content/contents/`, + } +} + +function mapV2RepresentationIntoV1(representation: any): Representation { + const { contents, bodyShapes, ...other } = representation + const newContents = contents.map(({ key, url }: { key: string; url: string }) => ({ + file: key, + hash: url.substring(url.lastIndexOf('/') + 1) + })) + return { + ...other, + bodyShapes, + contents: newContents + } +} \ No newline at end of file diff --git a/src/wearable/index.ts b/src/wearable/index.ts index b96fb36..eebb2ed 100644 --- a/src/wearable/index.ts +++ b/src/wearable/index.ts @@ -1,5 +1,36 @@ -import { Wearables } from "./types"; +import { getCatalystUrl, mapV2WearableIntoV1 } from "../shared/utils"; -export async function getListOfWearables(){ - return await fetch('https://wearable-api.decentraland.org/v2/collections').then(res => res.json()) as Wearables[] +export async function getListOfWearables(filters: AtLeastOne): Promise { + const queryParams = convertObjectToQueryParamString(filters) + const catalystUrl = await getCatalystUrl() + + return fetch(`${catalystUrl}/lambdas/collections/wearables${queryParams}`) + .then(res => res.json()) + .then(res => res.wearables) + .then(wearables => wearables.map(wearable => mapV2WearableIntoV1(catalystUrl, wearable))) +} + +function convertObjectToQueryParamString(object: Record):string { + let result = "" + for (const key in object) { + const value = object[key] + if (!value) continue + const name = key.substr(key.length - 1) === 's' ? key.slice(0, -1) : key + let values: string[] + if (Array.isArray(value)) { + values = [...value].map((_) => `${_}`) + } else { + values = [`${value}`] + } + result += result.length > 0 ? `?` : '&' + result += `${name}=` + values.join(`&${name}=`) + } + return result +} + +type WearableFilters = { + collectionIds: string[] + wearableIds: string[] + textSearch: string } +type AtLeastOne }> = Partial & U[keyof U] \ No newline at end of file From 18d74d54a11fe6857bf911133df32445bcc4a9a7 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 7 Apr 2021 15:40:42 -0300 Subject: [PATCH 2/4] Small null check --- src/avatar/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/avatar/index.ts b/src/avatar/index.ts index a75ba98..b7b24b8 100644 --- a/src/avatar/index.ts +++ b/src/avatar/index.ts @@ -51,9 +51,11 @@ export async function getUserInventory(address?: eth.Address) { const inventory: { amount: number, definition: any }[] = await response.json() const result: Wearable[] = [] for (const { definition, amount } of inventory) { - const mapped = mapV2WearableIntoV1(catalystUrl, definition) - for (let i = 0; i < amount; i++) { - result.push(mapped) + if (definition) { + const mapped = mapV2WearableIntoV1(catalystUrl, definition) + for (let i = 0; i < amount; i++) { + result.push(mapped) + } } } return result From b974982394ee5c1bfcab4a0524fd94264bbf9efa Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 7 Apr 2021 16:29:44 -0300 Subject: [PATCH 3/4] Minor fix --- src/wearable/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wearable/index.ts b/src/wearable/index.ts index eebb2ed..4f874f2 100644 --- a/src/wearable/index.ts +++ b/src/wearable/index.ts @@ -22,7 +22,7 @@ function convertObjectToQueryParamString(object: Record 0 ? `?` : '&' + result += result.length === 0 ? `?` : '&' result += `${name}=` + values.join(`&${name}=`) } return result From c75856ea412422dbbb9e45484637fbb463699f91 Mon Sep 17 00:00:00 2001 From: Nicolas Chamo Date: Wed, 7 Apr 2021 17:11:31 -0300 Subject: [PATCH 4/4] Update README --- README.md | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ed374ba..b57d8b9 100644 --- a/README.md +++ b/README.md @@ -805,7 +805,7 @@ crypto.avatar ### Get the rarity of the player's rarest item -Use the `rarestItem()` function to find out what's the rarest item that the player owns. +Use the `rarestItem()` function to find out what's the rarest item that the player owns. It returns the rarity category as a value from the `rarityLevel` enum. @@ -831,43 +831,26 @@ crypto.avatar ### Get data of all wearables -To fetch a full list of all wearables supported by Decentraland, including their full names, categories, contracts, etc, call the `getListOfWearables()`. This function doesn't take any arguments. +To fetch a list of wearables supported by Decentraland, including their full names, categories, contracts, etc, call the `getListOfWearables()`. This function supports the following filters: +```ts +{ + collectionIds: string[] + wearableIds: string[] + textSearch: string +} + +``` ```ts import * as crypto from '@dcl/crypto-scene-utils' executeTask(async () => { - const allWearables = await crypto.wearable.getListOfWearables() - log(allWearables) + const someWearables = await crypto.wearable.getListOfWearables({ collectionIds: ['urn:decentraland:ethereum:collections-v1:mf_sammichgamer'] }) + log(someWearables) }) ``` -This function returns an array of wearable collections, where each of these collections has a `wearables` field that contains a list of all wearables in that collection. Below is an extract of what this data looks like: - -```ts -[ {id: "halloween_2019", wearables: [ - { - baseUrl: "https://wearable-api.decentraland.org/v2/collections/halloween_2019/", - category: "earring", - description: "It may be someone else's head but that doesn't mean you can't look good", - hides: [], - i18n:0: [{code: "en", text: "Spider Earrings"}, {code: "es", text: "Pendientes de Araña"}], - id: "dcl://halloween_2019/bride_of_frankie_earring", - image: "QmZsnoehbtLDfk2FKbpDAk8nFatknSFQFqphF6RQu3Nkd7", - rarity: "mythic", - replaces: [], - representations: [{…}], - tags: (6) ["accesories", "exclusive", "earrings", "halloween", "spider", "exclusive"], - thumbnail: "QmSfe6dHYXAvsbMBTNGWwHtsr2aBoMjUrCW2TeLbCPw4oZ", - type: "wearable" - }, (...) - ] - }, - {id: "xmas_2019", wearables: [(...)]}, - (...) -] -``` - +This function returns an array of wearables. ---