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: support deployment of ens names #213

Merged
merged 9 commits into from
Sep 18, 2023
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
2 changes: 2 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ HTTP_SERVER_HOST=0.0.0.0

RPC_URL=https://rpc.decentraland.org/mainnet?project=worlds-content-server
MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace
#ENS_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/ensdomains/ens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is ok for this to be commented?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for now. So we don't have this enabled by default. If no var, no ens enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any case, we manage this via definitions.


SNS_ARN=

Expand Down Expand Up @@ -42,6 +43,7 @@ COMMS_FIXED_ADAPTER=ws-room:ws-room-service.decentraland.org/rooms/test-scene
DEPLOYMENT_TTL=300000
MAX_PARCELS=4
MAX_SIZE=100
ENS_MAX_SIZE=25
ALLOW_SDK6=false
WHITELIST_URL=https://config.decentraland.org/worlds-whitelist.json
NAME_VALIDATOR=DCL_NAME_CHECKER
Expand Down
85 changes: 4 additions & 81 deletions src/adapters/dcl-name-checker.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,18 @@
import { AppComponents, IWorldNamePermissionChecker } from '../types'
import { EthAddress } from '@dcl/schemas'
import LRU from 'lru-cache'
import { ContractFactory, RequestManager } from 'eth-connect'
import { checkerAbi, l1Contracts, L1Network } from '@dcl/catalyst-contracts'

type NamesResponse = {
nfts: { name: string; owner: { id: string } }[]
}

export const createDclNameChecker = (
components: Pick<AppComponents, 'logs' | 'marketplaceSubGraph'>
export const createNameChecker = (
components: Pick<AppComponents, 'logs' | 'nameOwnership'>
): IWorldNamePermissionChecker => {
const logger = components.logs.getLogger('check-permissions')
logger.info('Using TheGraph DclNameChecker')

const cache = new LRU<string, string | undefined>({
max: 100,
ttl: 5 * 60 * 1000, // cache for 5 minutes
fetchMethod: async (worldName: string): Promise<string | undefined> => {
/*
DCL owners are case-sensitive, so when searching by dcl name in TheGraph we
need to do a case-insensitive search because the worldName provided as fetch key
may not be in the exact same case of the registered name. There are several methods
suffixed _nocase, but not one for equality, so this is a bit hackish, but it works.
*/
const result = await components.marketplaceSubGraph.query<NamesResponse>(
`
query FetchOwnerForDclName($worldName: String) {
nfts(
where: {name_starts_with_nocase: $worldName, name_ends_with_nocase: $worldName, category: ens}
orderBy: name
first: 1000
) {
name
owner {
id
}
}
}`,
{
worldName: worldName.toLowerCase().replace('.dcl.eth', '')
}
)
logger.info(`Fetched owner of world ${worldName}: ${result.nfts.map(({ owner }) => owner.id.toLowerCase())}`)

const owners = result.nfts
.filter((nft) => `${nft.name.toLowerCase()}.dcl.eth` === worldName.toLowerCase())
.map(({ owner }) => owner.id.toLowerCase())

return owners.length > 0 ? owners[0] : undefined
}
})

const checkPermission = async (ethAddress: EthAddress, worldName: string): Promise<boolean> => {
if (worldName.length === 0) {
return false
}

const owner = await cache.fetch(worldName)
return !!owner && owner === ethAddress.toLowerCase()
}

return {
checkPermission
}
}

export const createOnChainDclNameChecker = async (
components: Pick<AppComponents, 'config' | 'logs' | 'ethereumProvider'>
): Promise<IWorldNamePermissionChecker> => {
const logger = components.logs.getLogger('check-permissions')
logger.info('Using OnChain DclNameChecker')
const ethNetwork = await components.config.requireString('ETH_NETWORK')
const contracts = l1Contracts[ethNetwork as L1Network]
if (!contracts) {
throw new Error(`Invalid ETH_NETWORK: ${ethNetwork}`)
}
const factory = new ContractFactory(new RequestManager(components.ethereumProvider), checkerAbi)
const checker = (await factory.at(contracts.checker)) as any

const checkPermission = async (ethAddress: EthAddress, worldName: string): Promise<boolean> => {
if (worldName.length === 0 || !worldName.endsWith('.dcl.eth')) {
return false
}

const hasPermission = await checker.checkName(
ethAddress,
contracts.registrar,
worldName.replace('.dcl.eth', ''),
'latest'
)
const owner = await components.nameOwnership.findOwner(worldName)
const hasPermission = !!owner && owner === ethAddress.toLowerCase()

logger.debug(`Checking name ${worldName} for address ${ethAddress}: ${hasPermission}`)

Expand Down
5 changes: 5 additions & 0 deletions src/adapters/limits-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export async function createLimitsManagerComponent({
const logger = logs.getLogger('limits-manager')
const hardMaxParcels = await config.requireNumber('MAX_PARCELS')
const hardMaxSize = await config.requireNumber('MAX_SIZE')
const hardMaxSizeForEns = await config.requireNumber('ENS_MAX_SIZE')
const hardAllowSdk6 = (await config.requireString('ALLOW_SDK6')) === 'true'
const whitelistUrl = await config.requireString('WHITELIST_URL')

Expand Down Expand Up @@ -51,6 +52,10 @@ export async function createLimitsManagerComponent({
return whitelist[worldName]?.max_parcels || hardMaxParcels
},
async getMaxAllowedSizeInMbFor(worldName: string): Promise<number> {
if (worldName.endsWith('.eth') && !worldName.endsWith('.dcl.eth')) {
return hardMaxSizeForEns
}

const whitelist = (await cache.fetch(CONFIG_KEY))!
return whitelist[worldName]?.max_size_in_mb || hardMaxSize
}
Expand Down
183 changes: 183 additions & 0 deletions src/adapters/name-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { AppComponents, INameOwnership } from '../types'
import { EthAddress } from '@dcl/schemas'
import { ContractFactory, RequestManager } from 'eth-connect'
import { l1Contracts, L1Network, registrarAbi } from '@dcl/catalyst-contracts'
import LRU from 'lru-cache'
import { createSubgraphComponent } from '@well-known-components/thegraph-component'

type NamesResponse = {
nfts: { name: string; owner: { id: string } }[]
}

async function createDclNameOwnership(
components: Pick<AppComponents, 'config' | 'ethereumProvider' | 'logs' | 'marketplaceSubGraph'>
) {
const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR')
switch (nameValidatorStrategy) {
case 'DCL_NAME_CHECKER':
return createMarketplaceSubgraphDclNameOwnership(components)
case 'ON_CHAIN_DCL_NAME_CHECKER':
return await createOnChainDclNameOwnership(components)

// Add more name validator strategies as needed here
}
throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`)
}

export async function createNameOwnership(
components: Pick<AppComponents, 'config' | 'ethereumProvider' | 'fetch' | 'logs' | 'marketplaceSubGraph' | 'metrics'>
): Promise<INameOwnership> {
const logger = components.logs.getLogger('name-ownership')
logger.info('Using NameOwnership')

const ensNameOwnership = await createEnsNameOwnership(components)
const dclNameOwnership = await createDclNameOwnership(components)

async function findOwner(worldName: string): Promise<EthAddress | undefined> {
const result =
worldName.endsWith('.eth') && !worldName.endsWith('.dcl.eth')
? await ensNameOwnership.findOwner(worldName)
: await dclNameOwnership.findOwner(worldName)
logger.info(`Fetched owner of world ${worldName}: ${result}`)
return result
}

return createCachingNameOwnership({ findOwner })
}

export async function createDummyNameOwnership(): Promise<INameOwnership> {
async function findOwner() {
return undefined
}
return {
findOwner
}
}

export async function createEnsNameOwnership(
components: Pick<AppComponents, 'config' | 'fetch' | 'logs' | 'metrics'>
): Promise<INameOwnership> {
const logger = components.logs.getLogger('ens-name-ownership')
logger.info('Using ENS NameOwnership')

const ensSubgraphUrl = await components.config.getString('ENS_SUBGRAPH_URL')
if (!ensSubgraphUrl) {
return await createDummyNameOwnership()
}

const ensSubGraph = await createSubgraphComponent(components, ensSubgraphUrl)
async function findOwner(ensName: string): Promise<EthAddress | undefined> {
const result = await ensSubGraph.query<NamesResponse>(
`query FetchOwnerForEnsName($ensName: String) {
nfts: domains(where: {name_in: [$ensName]}) {
name
owner {
id
}
}
}`,
{ ensName }
)

const owners = result.nfts.map(({ owner }) => owner.id.toLowerCase())
const owner = owners.length > 0 ? owners[0] : undefined

logger.debug(`Owner of ENS name '${ensName}' is ${owner}`)

return owner
}

return {
findOwner
}
}

export async function createMarketplaceSubgraphDclNameOwnership(
components: Pick<AppComponents, 'logs' | 'marketplaceSubGraph'>
): Promise<INameOwnership> {
const logger = components.logs.getLogger('marketplace-subgraph-dcl-name-ownership')
logger.info('Using Marketplace Subgraph NameOwnership')

async function findOwner(dclName: string): Promise<EthAddress | undefined> {
/*
DCL owners are case-sensitive, so when searching by dcl name in TheGraph we
need to do a case-insensitive search because the worldName provided as fetch key
may not be in the exact same case of the registered name. There are several methods
suffixed _nocase, but not one for equality, so this is a bit hackish, but it works.
*/
const result = await components.marketplaceSubGraph.query<NamesResponse>(
`query FetchOwnerForDclName($worldName: String) {
nfts(
where: {name_starts_with_nocase: $worldName, name_ends_with_nocase: $worldName, category: ens}
orderBy: name
first: 1000
) {
name
owner {
id
}
}
}`,

{ worldName: dclName.toLowerCase().replace('.dcl.eth', '') }
)

const owners = result.nfts
.filter((nft) => `${nft.name.toLowerCase()}.dcl.eth` === dclName.toLowerCase())
.map(({ owner }) => owner.id.toLowerCase())
return owners.length > 0 ? owners[0] : undefined
}

return {
findOwner
}
}

export async function createOnChainDclNameOwnership(
components: Pick<AppComponents, 'config' | 'logs' | 'ethereumProvider'>
): Promise<INameOwnership> {
const logger = components.logs.getLogger('on-chain-dcl-name-ownership')
logger.info('Using OnChain DCL NameOwnership')

const ethNetwork = (await components.config.requireString('ETH_NETWORK')) as L1Network
const contracts = l1Contracts[ethNetwork]
if (!contracts) {
throw new Error(`Invalid ETH_NETWORK: ${ethNetwork}`)
}
const requestManager = new RequestManager(components.ethereumProvider)
const registrarAddress = l1Contracts[ethNetwork].registrar
const factory = new ContractFactory(requestManager, registrarAbi)
const registrarContract = (await factory.at(registrarAddress)) as any

async function findOwner(dclName: string): Promise<EthAddress | undefined> {
try {
const owner = await registrarContract.getOwnerOf(dclName.replace('.dcl.eth', ''))
logger.debug(`Owner of DCL name '${dclName}' is ${owner}`)
return owner
} catch (e) {
return undefined
}
}

return {
findOwner
}
}

export async function createCachingNameOwnership(nameOwnership: INameOwnership): Promise<INameOwnership> {
const cache = new LRU<string, EthAddress | undefined>({
max: 100,
ttl: 60 * 1000, // cache for 1 minute
fetchMethod: async (worldName: string): Promise<string | undefined> => {
return await nameOwnership.findOwner(worldName)
}
})

async function findOwner(name: string): Promise<EthAddress | undefined> {
return await cache.fetch(name)
}

return {
findOwner
}
}
30 changes: 11 additions & 19 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
createFsComponent
} from '@dcl/catalyst-storage'
import { createStatusComponent } from './adapters/status'
import { createDclNameChecker, createOnChainDclNameChecker } from './adapters/dcl-name-checker'
import { createLimitsManagerComponent } from './adapters/limits-manager'
import { createWorldsManagerComponent } from './adapters/worlds-manager'
import { createCommsAdapterComponent } from './adapters/comms-adapter'
Expand All @@ -32,21 +31,8 @@ import { createMigrationExecutor } from './migrations/migration-executor'
import { createNameDenyListChecker } from './adapters/name-deny-list-checker'
import { createDatabaseComponent } from './adapters/database-component'
import { createPermissionsManagerComponent } from './adapters/permissions-manager'

async function determineNameValidator(
components: Pick<AppComponents, 'config' | 'ethereumProvider' | 'logs' | 'marketplaceSubGraph'>
) {
const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR')
switch (nameValidatorStrategy) {
case 'DCL_NAME_CHECKER':
return createDclNameChecker(components)
case 'ON_CHAIN_DCL_NAME_CHECKER':
return await createOnChainDclNameChecker(components)

// Add more name validator strategies as needed here
}
throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`)
}
import { createNameOwnership } from './adapters/name-ownership'
import { createNameChecker } from './adapters/dcl-name-checker'

// Initialize all the components of the app
export async function initComponents(): Promise<AppComponents> {
Expand Down Expand Up @@ -75,7 +61,6 @@ export async function initComponents(): Promise<AppComponents> {

const subGraphUrl = await config.requireString('MARKETPLACE_SUBGRAPH_URL')
const marketplaceSubGraph = await createSubgraphComponent({ config, logs, metrics, fetch }, subGraphUrl)

const snsArn = await config.getString('SNS_ARN')

const status = await createStatusComponent({ logs, fetch, config })
Expand All @@ -90,11 +75,17 @@ export async function initComponents(): Promise<AppComponents> {
logs
})

const namePermissionChecker: IWorldNamePermissionChecker = await determineNameValidator({
const nameOwnership = await createNameOwnership({
config,
ethereumProvider,
fetch,
logs,
marketplaceSubGraph,
metrics
})
const namePermissionChecker: IWorldNamePermissionChecker = createNameChecker({
logs,
marketplaceSubGraph
nameOwnership
})

const limitsManager = await createLimitsManagerComponent({ config, fetch, logs })
Expand Down Expand Up @@ -130,6 +121,7 @@ export async function initComponents(): Promise<AppComponents> {
metrics,
migrationExecutor,
nameDenyListChecker,
nameOwnership,
namePermissionChecker,
permissionsManager,
server,
Expand Down
Loading