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

feat: 1st draft of integrating ownership-server #209

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ SUBGRAPH_COMPONENT_QUERY_TIMEOUT=60000
SUBGRAPH_COMPONENT_RETRIES=1

DISABLE_THIRD_PARTY_PROVIDERS_RESOLVER_SERVICE_USAGE=false

#OWNERSHIP_SERVER_BASE_URL=
20 changes: 2 additions & 18 deletions src/controllers/handlers/profiles-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ import { Profile } from '@dcl/catalyst-api-specs/lib/client'

export async function profilesHandler(
context: HandlerContextWithPath<
| 'metrics'
| 'content'
| 'theGraph'
| 'config'
| 'fetch'
| 'ownershipCaches'
| 'thirdPartyProvidersStorage'
| 'logs'
| 'metrics',
'content' | 'theGraph' | 'config' | 'fetch' | 'ownershipCaches' | 'thirdPartyProvidersStorage' | 'logs' | 'metrics',
'/profiles'
>
): Promise<{ status: 200; body: Profile[] } | { status: 304 }> {
Expand Down Expand Up @@ -46,15 +38,7 @@ export async function profilesHandler(

export async function profileHandler(
context: HandlerContextWithPath<
| 'metrics'
| 'content'
| 'theGraph'
| 'config'
| 'fetch'
| 'ownershipCaches'
| 'thirdPartyProvidersStorage'
| 'logs'
| 'metrics',
'content' | 'theGraph' | 'config' | 'fetch' | 'ownershipCaches' | 'thirdPartyProvidersStorage' | 'logs' | 'metrics',
'/profile/:id'
>
): Promise<{ status: 200; body: Profile }> {
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from './handlers/third-party-wearables-handler'
import { wearablesHandler } from './handlers/wearables-handler'
import { explorerHandler } from './handlers/explorer-handler'
import { errorHandler } from './handlers/errorHandler'
import { errorHandler } from './handlers/error-handler'
import { aboutHandler } from './handlers/about-handler'
import { outfitsHandler } from './handlers/outfits-handler'
import { getCatalystServersHandler } from './handlers/catalyst-servers-handler'
Expand Down
2 changes: 1 addition & 1 deletion src/logic/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import LRU from 'lru-cache'

/*
* Reads the provided map and returns those nfts that are cached and those that are unknown.
* The cache must be {adress -> {nft -> isOwned} }.
* The cache must be {address -> {nft -> isOwned} }.
*/
export function getCachedNFTsAndPendingCheckNFTs(
ownedNFTsByAddress: Map<string, string[]>,
Expand Down
131 changes: 126 additions & 5 deletions src/logic/ownership.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,95 @@
import { TheGraphComponent } from '../ports/the-graph'
import { AppComponents } from '../types'
import { createFetchComponent } from '../ports/fetch'
import { IMetricsComponent } from '@well-known-components/interfaces'
import { metricDeclarations } from '../metrics'

function generateRandomId(length: number) {
const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let randomId = ''

for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length)
randomId += characters.charAt(randomIndex)
}

return randomId
}

async function withTime<T>(
metrics: IMetricsComponent<keyof typeof metricDeclarations>,
id: string,
which: string,
what: () => Promise<T>
) {
const start = Date.now()
const timer = metrics.startTimer('ownership_check_duration_seconds', { id })

try {
return await what()
} finally {
const end = Date.now()
timer.end({ id })
console.log(`${which} took ${end - start}ms`)
}
}

function eqSet(xs: Set<string>, ys: Set<string>) {
return xs.size === ys.size && [...xs].every((x) => ys.has(x))
}

function equal(actual: Map<string, string[]>, expected: Map<string, string[]>) {
if (!eqSet(new Set(actual.keys()), new Set(expected.keys()))) {
return false
}

for (const [ethAddress, nfts] of actual) {
const expectedNfts = expected.get(ethAddress)
if (!expectedNfts || !eqSet(new Set(nfts), new Set(expectedNfts))) {
return false
}
}

return true
}

/*
* Checks the ownership for every nft resulting in a map of ownership for every eth address.
* Receive a `querySubgraph` method to know how to do the query.
*/
export async function ownedNFTsByAddress(
components: Pick<AppComponents, 'theGraph' | 'config'>,
components: Pick<AppComponents, 'config' | 'metrics' | 'theGraph'>,
nftIdsByAddressToCheck: Map<string, string[]>,
querySubgraph: (theGraph: TheGraphComponent, nftsToCheck: [string, string[]][]) => any
): Promise<Map<string, string[]>> {
// Check ownership for unknown nfts
const ownedNftIdsByEthAddress = await querySubgraphByFragments(components, nftIdsByAddressToCheck, querySubgraph)
const requestId = generateRandomId(10)
const ownedNftIdsByEthAddress = await withTime<Map<string, string[]>>(
components.metrics,
'the-graph',
`[${requestId}] theGraph `,
() => querySubgraphByFragments(components, nftIdsByAddressToCheck, querySubgraph)
)

const ownershipServerBaseUrl = await components.config.getString('OWNERSHIP_SERVER_BASE_URL')
try {
if (ownershipServerBaseUrl) {
const ownershipIndex = await withTime<Map<string, string[]>>(
components.metrics,
'ownership-server',
`[${requestId}] ownership `,
() => queryOwnershipIndex(components, nftIdsByAddressToCheck)
)
if (!equal(ownedNftIdsByEthAddress, ownershipIndex)) {
console.log('different results', {
theGraph: JSON.stringify(Object.fromEntries(ownedNftIdsByEthAddress)),
ownershipIndex: JSON.stringify(Object.fromEntries(ownershipIndex))
})
}
}
} catch (error) {
console.log('error', error)
}

// Fill the final map with nfts ownership
for (const [ethAddress, nfts] of nftIdsByAddressToCheck) {
Expand All @@ -24,6 +102,49 @@ export async function ownedNFTsByAddress(
return ownedNftIdsByEthAddress
}

export type OwnsItemsByAddressSingle = {
address: string
itemUrns: string[]
}

/**
* Return a set of the NFTs that are actually owned by the eth address, for every eth address, based on ownership-server
*/
async function queryOwnershipIndex(
components: Pick<AppComponents, 'config'>,
nftIdsByAddressToCheck: Map<string, string[]>
): Promise<Map<string, string[]>> {
const { config } = components
const fetch = await createFetchComponent()
const result: Map<string, string[]> = new Map()

const itemUrnsByAddress: OwnsItemsByAddressSingle[] = []

for (const [ethAddress, nfts] of nftIdsByAddressToCheck.entries()) {
if (nfts.length === 0) {
result.set(ethAddress, [])
continue
}
itemUrnsByAddress.push({ address: ethAddress, itemUrns: nfts })
}

if (itemUrnsByAddress.length > 0) {
const ownershipServerBaseUrl = await config.requireString('OWNERSHIP_SERVER_BASE_URL')
const response = await fetch.fetch(`${ownershipServerBaseUrl}/ownsItemsByAddress`, {
method: 'POST',
body: JSON.stringify({ itemUrnsByAddress }),
timeout: 1000
})
if (response.ok) {
const json = await response.json()
for (const { address, itemUrns } of json.itemUrnsByAddress) {
result.set(address, itemUrns)
}
}
}
return result
}

/*
* Return a set of the NFTs that are actually owned by the eth address, for every eth address.
* Receive a `querySubgraph` method to know how to do the query.
Expand All @@ -34,14 +155,14 @@ async function querySubgraphByFragments(
querySubgraph: (theGraph: TheGraphComponent, nftsToCheck: [string, string[]][]) => any
): Promise<Map<string, string[]>> {
const { theGraph, config } = components
const nft_fragments_per_query = parseInt((await config.getString('NFT_FRAGMENTS_PER_QUERY')) ?? '10')
const nftFragmentsPerQuery = parseInt((await config.getString('NFT_FRAGMENTS_PER_QUERY')) ?? '10')
const entries = Array.from(nftIdsByAddressToCheck.entries())
const result: Map<string, string[]> = new Map()

// Make multiple queries to graph as at most NFT_FRAGMENTS_PER_QUERY per time
let offset = 0
while (offset < entries.length) {
const slice = entries.slice(offset, offset + nft_fragments_per_query)
const slice = entries.slice(offset, offset + nftFragmentsPerQuery)
try {
const queryResult = await querySubgraph(theGraph, slice)
for (const { ownedNFTs, owner } of queryResult) {
Expand All @@ -51,7 +172,7 @@ async function querySubgraphByFragments(
// TODO: logger
console.log(error)
} finally {
offset += nft_fragments_per_query
offset += nftFragmentsPerQuery
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const metricDeclarations = {
help: 'Third Party Provider fetch assets request duration in seconds.',
type: IMetricsComponent.HistogramType,
labelNames: ['id']
},
ownership_check_duration_seconds: {
help: 'Ownership check duration in seconds.',
type: IMetricsComponent.HistogramType,
labelNames: ['id']
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/ports/ownership-checker/wearables-ownership-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fillCacheWithRecentlyCheckedWearables, getCachedNFTsAndPendingCheckNFTs
import { mergeMapIntoMap } from '../../logic/maps'

export function createWearablesOwnershipChecker(
components: Pick<AppComponents, 'metrics' | 'content' | 'theGraph' | 'config' | 'ownershipCaches'>
components: Pick<AppComponents, 'config' | 'content' | 'metrics' | 'ownershipCaches' | 'theGraph'>
): NFTsOwnershipChecker {
let ownedWearablesByAddress: Map<string, string[]> = new Map()
const cache = components.ownershipCaches.wearablesCache
Expand Down Expand Up @@ -119,7 +119,6 @@ async function getOwnersByWearable(
urns: wearables.map((urnObj: { urn: string }) => urnObj.urn)
}))
}

return wearableIdsToCheck.map(([owner]) => ({ owner, urns: [] }))
}

Expand Down
Loading