Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make library work with v2 wearables #9

Merged
merged 5 commits into from
Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 13 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

---

Expand Down
89 changes: 52 additions & 37 deletions src/avatar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -28,45 +27,48 @@ 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
*
* @param 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) {
if (definition) {
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
*
* @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)
}

/**
Expand All @@ -76,15 +78,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
Expand Down Expand Up @@ -112,7 +116,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) {
Expand All @@ -131,7 +135,7 @@ let rarestEquippedItem: rarityLevel = 0
log(rarityLevel[rarestEquippedItem])
return rarestEquippedItem
}

export function updateRarity(rarity: Rarity) {
let rarityNum: number = 0
switch (rarity) {
Expand Down Expand Up @@ -162,7 +166,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
Expand All @@ -174,11 +178,22 @@ export async function getPlayerSnapshots(playerId?: string) :Promise<Snapshots|n
const profile = await getUserInfo()
playerId = profile.id
}
const realm = await getCurrentRealm().then((r: any) =>
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)))
}
}

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}`
}
}
43 changes: 43 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getCurrentRealm } from "@decentraland/EnvironmentAPI"
import { Representation, Type, Wearable } from "../wearable/types"

export function getCatalystUrl(): Promise<string> {
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
}
}
37 changes: 34 additions & 3 deletions src/wearable/index.ts
Original file line number Diff line number Diff line change
@@ -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<WearableFilters>): Promise<Wearable[]> {
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, number | boolean | string | number[] | boolean[] | string[]>):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<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]