Skip to content

Commit

Permalink
feat: linked wearables v2 (#346)
Browse files Browse the repository at this point in the history
  • Loading branch information
marianogoldman authored Aug 13, 2024
1 parent dfb40a4 commit 449d72f
Show file tree
Hide file tree
Showing 31 changed files with 1,144 additions and 174 deletions.
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

#NFT_WORKER_BASE_URL=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
**/.DS_Store
coverage
.env
third-party-contracts-*.json
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,38 @@ The layer that communicates with the outside world, such as http, kafka, and the
We use the components abstraction to organize our ports (e.g. HTTP client, database client, redis client) and any other logic that needs to track mutable state or encode dependencies between stateful components. For every environment (e.g. test, e2e, prod, staging...) we have a different version of our component systems, enabling us to easily inject mocks or different implementations for different contexts.

We make components available to incoming http and kafka handlers. For instance, the http-server handlers have access to things like the database or HTTP components, and pass them down to the controller level for general use.

### Sequence diagram of for backpack building
```mermaid
sequenceDiagram
actor User
participant Catalyst
participant Graph as Third Party Resolvers
participant Alchemy as Alchemy API
User ->> Catalyst: Get all wearables I own
activate Catalyst
Catalyst ->> Graph: Get all active Third Party Providers
Graph -->> Catalyst: Ok (HTTP 200) with the list of TPAs
Catalyst ->> Catalyst: Build list of contracts to query
Catalyst ->> Alchemy: Get owned wearables for 0x123 in those contracts
Alchemy -->> Catalyst: Ok (HTTP 200) with the wearables
Catalyst ->> Catalyst: For each NFT owned returned from Alchemy<br>assign the corresponding wearable (based on mappings)
deactivate Catalyst
Catalyst ->> User: Ok (HTTP 200) with the wearables
```


### Sequence diagram for ownership checking during profile validation
```mermaid
sequenceDiagram
actor User
participant Catalyst
participant Blockchain
User ->> Catalyst: Get my profile
activate Catalyst
Catalyst ->> Blockchain: Does 0x123 own this red shirt, this blue hat and the yellow shoes?
Blockchain->> Catalyst: Ok: yes, no, yes
Catalyst ->> Catalyst: Remove all non-owned wearables
deactivate Catalyst
Catalyst ->> User: Ok (HTTP 200) with the profile
```
58 changes: 58 additions & 0 deletions src/adapters/alchemy-nft-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AppComponents } from '../types'
import { ContractNetwork } from '@dcl/schemas'

export type AlchemyNftFetcher = {
getNFTsForOwner(owner: string, contractsByNetwork: Record<string, Set<string>>): Promise<string[]>
}

export async function createAlchemyNftFetcher({
config,
logs,
fetch
}: Pick<AppComponents, 'config' | 'logs' | 'fetch'>): Promise<AlchemyNftFetcher> {
const logger = logs.getLogger('alchemy-nft-fetcher')
const nftWorkerBaseUrl = (await config.getString('NFT_WORKER_BASE_URL')) || 'https://nfts.decentraland.org'

async function getNFTsForOwnerForNetwork(
owner: string,
network: string,
contractAddresses: Set<string>
): Promise<string[]> {
if (!Object.values(ContractNetwork).includes(network as ContractNetwork)) {
logger.warn(`Network ${network} not supported for LinkedWearables`)
return []
}

const response = await fetch.fetch(`${nftWorkerBaseUrl}/wallets/${owner}/networks/${network}/nfts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(Array.from(contractAddresses))
})

if (!response.ok) {
logger.error(`Error fetching NFTs from Alchemy: ${response.status} - ${response.statusText}`)
return []
}

const nfts = await response.json()

return nfts.data as string[]
}

async function getNFTsForOwner(owner: string, contractsByNetwork: Record<string, Set<string>>): Promise<string[]> {
const all = await Promise.all(
Object.entries(contractsByNetwork).map(([network, contractAddresses]) =>
getNFTsForOwnerForNetwork(owner, network, contractAddresses)
)
)

return all.flat(1)
}

return {
getNFTsForOwner
}
}
5 changes: 5 additions & 0 deletions src/adapters/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,17 @@ export type IProfilesComponent = {
export async function createProfilesComponent(
components: Pick<
AppComponents,
| 'alchemyNftFetcher'
| 'metrics'
| 'content'
| 'contentServerUrl'
| 'entitiesFetcher'
| 'theGraph'
| 'config'
| 'fetch'
| 'ownershipCaches'
| 'l1ThirdPartyItemChecker'
| 'l2ThirdPartyItemChecker'
| 'thirdPartyProvidersStorage'
| 'logs'
| 'wearablesFetcher'
Expand Down
15 changes: 13 additions & 2 deletions src/adapters/third-party-providers-graph-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppComponents, ThirdPartyProvider } from '../types'
import { sanitizeContractList } from '../logic/utils'

export type ThirdPartyProvidersGraphFetcher = {
get(): Promise<ThirdPartyProvider[]>
Expand All @@ -17,6 +18,10 @@ const QUERY_ALL_THIRD_PARTY_RESOLVERS = `
thirdParty {
name
description
contracts {
network
address
}
}
}
}
Expand All @@ -28,12 +33,18 @@ export function createThirdPartyProvidersGraphFetcherComponent({
}: Pick<AppComponents, 'theGraph'>): ThirdPartyProvidersGraphFetcher {
return {
async get(): Promise<ThirdPartyProvider[]> {
return (
const thirdPartyProviders = (
await theGraph.thirdPartyRegistrySubgraph.query<ThirdPartyResolversQueryResults>(
QUERY_ALL_THIRD_PARTY_RESOLVERS,
{}
)
).thirdParties.filter((thirdParty) => thirdParty.id.includes('collections-thirdparty'))
).thirdParties

if (thirdPartyProviders) {
sanitizeContractList(thirdPartyProviders)
}

return thirdPartyProviders
}
}
}
26 changes: 17 additions & 9 deletions src/adapters/third-party-providers-service-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { L2Network } from '@dcl/catalyst-contracts'
import { AppComponents, ThirdPartyProvider } from '../types'
import { sanitizeContractList } from '../logic/utils'

export type ThirdPartyProvidersServiceFetcher = {
get(): Promise<ThirdPartyProvider[]>
Expand All @@ -23,15 +24,22 @@ export async function createThirdPartyProvidersServiceFetcherComponent(
const isThirdPartyProvidersResolverServiceDisabled: boolean =
(await config.getString('DISABLE_THIRD_PARTY_PROVIDERS_RESOLVER_SERVICE_USAGE')) === 'true'

return {
async get(): Promise<ThirdPartyProvider[]> {
if (isThirdPartyProvidersResolverServiceDisabled) {
throw new Error(
'Third Party Providers resolver service will not be used since DISABLE_THIRD_PARTY_PROVIDERS_RESOLVER_SERVICE_USAGE is set'
)
}
const response: ThirdPartyProvidersServiceResponse = await (await fetch.fetch(`${serviceUrl}/providers`)).json()
return response.thirdPartyProviders
async function get(): Promise<ThirdPartyProvider[]> {
if (isThirdPartyProvidersResolverServiceDisabled) {
throw new Error(
'Third Party Providers resolver service will not be used since DISABLE_THIRD_PARTY_PROVIDERS_RESOLVER_SERVICE_USAGE is set'
)
}
const response: ThirdPartyProvidersServiceResponse = await (await fetch.fetch(`${serviceUrl}/providers`)).json()

if (response.thirdPartyProviders) {
sanitizeContractList(response.thirdPartyProviders)
}

return response.thirdPartyProviders
}

return {
get
}
}
35 changes: 32 additions & 3 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import { createThirdPartyProvidersServiceFetcherComponent } from './adapters/thi
import { createThirdPartyProvidersStorage } from './logic/third-party-providers-storage'
import { createProfilesComponent } from './adapters/profiles'
import { IFetchComponent } from '@well-known-components/interfaces'
import { createAlchemyNftFetcher } from './adapters/alchemy-nft-fetcher'
import { createThirdPartyContractRegistry } from './ports/ownership-checker/third-party-contract-registry'
import { createThirdPartyItemChecker } from './ports/ownership-checker/third-party-item-checker'

// Initialize all the components of the app
export async function initComponents(
Expand Down Expand Up @@ -112,6 +115,19 @@ export async function initComponents(
const poisFetcher = await createPOIsFetcher({ l2Provider }, l2Network)
const nameDenylistFetcher = await createNameDenylistFetcher({ l1Provider }, l1Network)

const l1ThirdPartyContractRegistry = await createThirdPartyContractRegistry(logs, l1Provider, l1Network as any, '.')
const l2ThirdPartyContractRegistry = await createThirdPartyContractRegistry(logs, l2Provider, l2Network as any, '.')
const l1ThirdPartyItemChecker = await createThirdPartyItemChecker(
{ entitiesFetcher, logs },
l1Provider,
l1ThirdPartyContractRegistry
)
const l2ThirdPartyItemChecker = await createThirdPartyItemChecker(
{ entitiesFetcher, logs },
l2Provider,
l2ThirdPartyContractRegistry
)

const thirdPartyProvidersGraphFetcher = createThirdPartyProvidersGraphFetcherComponent({ theGraph })
const thirdPartyProvidersServiceFetcher = await createThirdPartyProvidersServiceFetcherComponent(
{ config, fetch },
Expand All @@ -123,12 +139,20 @@ export async function initComponents(
thirdPartyProvidersServiceFetcher
})
const thirdPartyWearablesFetcher = createElementsFetcherComponent({ logs }, async (address) =>
fetchAllThirdPartyWearables({ thirdPartyProvidersStorage, fetch, logs, entitiesFetcher, metrics }, address)
fetchAllThirdPartyWearables(
{ alchemyNftFetcher, contentServerUrl, thirdPartyProvidersStorage, fetch, logs, entitiesFetcher, metrics },
address
)
)

const alchemyNftFetcher = await createAlchemyNftFetcher({ config, logs, fetch })

const profiles = await createProfilesComponent({
alchemyNftFetcher,
metrics,
content,
contentServerUrl,
entitiesFetcher,
theGraph,
config,
fetch,
Expand All @@ -137,7 +161,9 @@ export async function initComponents(
logs,
wearablesFetcher,
emotesFetcher,
namesFetcher
namesFetcher,
l1ThirdPartyItemChecker,
l2ThirdPartyItemChecker
})

return {
Expand Down Expand Up @@ -170,6 +196,9 @@ export async function initComponents(
catalystsFetcher,
poisFetcher,
nameDenylistFetcher,
profiles
profiles,
alchemyNftFetcher,
l1ThirdPartyItemChecker,
l2ThirdPartyItemChecker
}
}
5 changes: 5 additions & 0 deletions src/controllers/handlers/outfits-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import { Entity } from '@dcl/schemas'

export async function outfitsHandler(
context: HandlerContextWithPath<
| 'alchemyNftFetcher'
| 'metrics'
| 'content'
| 'contentServerUrl'
| 'entitiesFetcher'
| 'theGraph'
| 'config'
| 'fetch'
| 'ownershipCaches'
| 'wearablesFetcher'
| 'namesFetcher'
| 'l1ThirdPartyItemChecker'
| 'l2ThirdPartyItemChecker'
| 'thirdPartyProvidersStorage'
| 'logs',
'/outfits/:id'
Expand Down
Loading

0 comments on commit 449d72f

Please sign in to comment.