generated from well-known-components/template-server
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* wip * wip * fix * wip * wip * wip * fix * fix * names * test: add tests for GET /users/:address/wearables * add tests for definitions * add tests for error cases on GET /users/:address/wearables * add cache tests for wearables endpoint * chore: emotes (#122) * chore: add emotes just duplicated all logic from wearables * chore: abstraction in definitions-fetcher * chore: abstract wearablesFetcher and emotesFetcher in itemsFetcher * chore: fix tests * fix: tests * chore: item-fetcher queries record * test: add emotes integration tests (#124) * chore: create test components with local fetch and the graph mock to avoid external calls * test: add emote integration tests * chore: add types to test function * chore: add integration tests for names * chore: change ropsten to goerli * feat: add LANDs endpoint * test: add lands integration tests * chore: remove old definitions logic * chore: delete nfts.ts unused code * chore: remove commented code * chore: readd old endpoints (#127) * chore: re add old endpoints * chore: small stuff * chore: remove non used functions and dont export functions * chore: stuff * chore: stuff --------- Co-authored-by: Hugo Arregui <[email protected]> Co-authored-by: Alejo Thomas Ortega <[email protected]>
- Loading branch information
1 parent
393a60a
commit 96d5e27
Showing
44 changed files
with
4,228 additions
and
1,677 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import LRU from 'lru-cache' | ||
import { IBaseComponent } from '@well-known-components/interfaces' | ||
import { AppComponents, Definition } from '../types' | ||
import { extractEmoteDefinitionFromEntity, extractWearableDefinitionFromEntity } from './definitions' | ||
import { Entity } from '@dcl/schemas' | ||
|
||
export type DefinitionsFetcher = IBaseComponent & { | ||
fetchWearablesDefinitions(urns: string[]): Promise<(Definition | undefined)[]> | ||
fetchEmotesDefinitions(urns: string[]): Promise<(Definition | undefined)[]> | ||
} | ||
|
||
export async function createDefinitionsFetcherComponent({ | ||
config, | ||
content | ||
}: Pick<AppComponents, 'logs' | 'config' | 'content'>): Promise<DefinitionsFetcher> { | ||
const itemsSize = (await config.getNumber('ITEMS_CACHE_MAX_SIZE')) ?? 1000 | ||
const itemsAge = (await config.getNumber('ITEMS_CACHE_MAX_AGE')) ?? 600000 // 10 minutes by default | ||
|
||
// TODO create lower case cache, get/set wrapped to set the key to lowercase | ||
const itemDefinitionsCache = new LRU<string, Definition>({ | ||
max: itemsSize, | ||
ttl: itemsAge | ||
}) | ||
|
||
async function fetchItemsDefinitions( | ||
urns: string[], | ||
mapEntityToDefinition: (components: Pick<AppComponents, 'content'>, entity: Entity) => Definition | ||
): Promise<(Definition | undefined)[]> { | ||
const nonCachedURNs: string[] = [] | ||
for (const urn of urns) { | ||
const definition = itemDefinitionsCache.get(urn.toLowerCase()) | ||
if (!definition) { | ||
nonCachedURNs.push(urn) | ||
} | ||
} | ||
|
||
if (nonCachedURNs.length !== 0) { | ||
const entities = await content.fetchEntitiesByPointers(nonCachedURNs) | ||
for (const entity of entities) { | ||
const definition = mapEntityToDefinition({ content }, entity) | ||
itemDefinitionsCache.set(definition.id.toLowerCase(), definition) | ||
} | ||
} | ||
|
||
return urns.map((urn) => itemDefinitionsCache.get(urn.toLowerCase())) | ||
} | ||
|
||
return { | ||
fetchWearablesDefinitions: (urns) => fetchItemsDefinitions(urns, extractWearableDefinitionFromEntity), | ||
fetchEmotesDefinitions: (urns) => fetchItemsDefinitions(urns, extractEmoteDefinitionFromEntity) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import LRU from 'lru-cache' | ||
import { AppComponents, Limits, ItemsResult, Item, ItemFetcher, ItemFetcherError, ItemFetcherErrorCode } from '../types' | ||
import { ISubgraphComponent } from '@well-known-components/thegraph-component' | ||
import { compareByRarity } from '../logic/utils' | ||
|
||
const THE_GRAPH_PAGE_SIZE = 1000 | ||
|
||
// TODO cache metrics | ||
type ItemsQueryResponse = { | ||
nfts: ItemFromQuery[] | ||
} | ||
|
||
export type ItemFromQuery = { | ||
urn: string | ||
id: string | ||
tokenId: string | ||
transferredAt: number | ||
item: { | ||
rarity: string | ||
price: number | ||
} | ||
} | ||
|
||
const QUERIES: Record<ItemCategory, string> = { | ||
emote: createQueryForCategory('emote'), | ||
wearable: createQueryForCategory('wearable') | ||
} | ||
|
||
function groupItemsByURN(items: ItemFromQuery[]): Item[] { | ||
const itemsByURN = new Map<string, Item>() | ||
|
||
items.forEach((item) => { | ||
const individualData = { | ||
id: item.id, | ||
tokenId: item.tokenId, | ||
transferredAt: item.transferredAt, | ||
price: item.item.price | ||
} | ||
if (itemsByURN.has(item.urn)) { | ||
const itemFromMap = itemsByURN.get(item.urn)! | ||
itemFromMap.individualData.push(individualData) | ||
itemFromMap.amount = itemFromMap.amount + 1 | ||
} else { | ||
itemsByURN.set(item.urn, { | ||
urn: item.urn, | ||
individualData: [individualData], | ||
rarity: item.item.rarity, | ||
amount: 1 | ||
}) | ||
} | ||
}) | ||
|
||
return Array.from(itemsByURN.values()) | ||
} | ||
|
||
type ItemCategory = 'wearable' | 'emote' | ||
|
||
function createQueryForCategory(category: ItemCategory) { | ||
return `query fetchItemsByOwner($owner: String, $idFrom: String) { | ||
nfts( | ||
where: { id_gt: $idFrom, owner: $owner, category: "${category}"}, | ||
orderBy: id, | ||
orderDirection: asc, | ||
first: ${THE_GRAPH_PAGE_SIZE} | ||
) { | ||
urn, | ||
id, | ||
tokenId, | ||
category | ||
transferredAt, | ||
item { | ||
rarity, | ||
price | ||
} | ||
} | ||
}` | ||
} | ||
|
||
async function runItemsQuery( | ||
subgraph: ISubgraphComponent, | ||
address: string, | ||
category: ItemCategory | ||
): Promise<ItemFromQuery[]> { | ||
const items = [] | ||
const owner = address.toLowerCase() | ||
let idFrom = '' | ||
let result: ItemsQueryResponse | ||
const query = QUERIES[category] | ||
do { | ||
result = await subgraph.query<ItemsQueryResponse>(query, { | ||
owner, | ||
idFrom | ||
}) | ||
|
||
if (result.nfts.length === 0) { | ||
break | ||
} | ||
|
||
for (const nft of result.nfts) { | ||
items.push(nft) | ||
} | ||
|
||
idFrom = items[items.length - 1].id | ||
} while (result.nfts.length === THE_GRAPH_PAGE_SIZE) | ||
return items | ||
} | ||
|
||
export async function createWearableFetcherComponent(components: Pick<AppComponents, 'logs' | 'config' | 'theGraph'>) { | ||
return createItemFetcherComponent(components, 'wearable', true) | ||
} | ||
|
||
export async function createEmoteFetcherComponent(components: Pick<AppComponents, 'logs' | 'config' | 'theGraph'>) { | ||
return createItemFetcherComponent(components, 'emote', false) | ||
} | ||
|
||
async function createItemFetcherComponent( | ||
{ config, theGraph, logs }: Pick<AppComponents, 'logs' | 'config' | 'theGraph'>, | ||
category: ItemCategory, | ||
includeEthereum: boolean | ||
): Promise<ItemFetcher> { | ||
const itemsSize = (await config.getNumber('ITEMS_CACHE_MAX_SIZE')) ?? 1000 | ||
const itemsAge = (await config.getNumber('ITEMS_CACHE_MAX_AGE')) ?? 600000 // 10 minutes by default | ||
const logger = logs.getLogger(`${category}-fetcher`) | ||
|
||
const cache = new LRU<string, Item[]>({ | ||
max: itemsSize, | ||
ttl: itemsAge, | ||
fetchMethod: async function (address: string, staleValue: Item[]) { | ||
try { | ||
const [ethereumItems, maticItems] = await Promise.all([ | ||
includeEthereum | ||
? runItemsQuery(theGraph.ethereumCollectionsSubgraph, address, category) | ||
: ([] as ItemFromQuery[]), | ||
runItemsQuery(theGraph.maticCollectionsSubgraph, address, category) | ||
]) | ||
|
||
return groupItemsByURN(ethereumItems.concat(maticItems)).sort(compareByRarity) | ||
} catch (err: any) { | ||
logger.error(err) | ||
return staleValue | ||
} | ||
} | ||
}) | ||
|
||
async function fetchByOwner(address: string, { offset, limit }: Limits): Promise<ItemsResult> { | ||
const results = await cache.fetch(address) | ||
if (results === undefined) { | ||
throw new ItemFetcherError(ItemFetcherErrorCode.CANNOT_FETCH_ITEMS, `Cannot fetch ${category}s for ${address}`) | ||
} | ||
const totalAmount = results.length | ||
return { | ||
items: results.slice(offset, offset + limit), | ||
totalAmount | ||
} | ||
} | ||
|
||
return { | ||
fetchByOwner | ||
} | ||
} |
Oops, something went wrong.