Skip to content

Commit

Permalink
feat: add contributable domains endpoint (#294)
Browse files Browse the repository at this point in the history
Co-authored-by: Mariano Goldman <[email protected]>
  • Loading branch information
Melisa Anabella Rossi and marianogoldman authored May 23, 2024
1 parent 0db9816 commit 694527f
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 2 deletions.
33 changes: 31 additions & 2 deletions src/adapters/worlds-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { AppComponents, IPermissionChecker, IWorldsManager, Permissions, WorldMetadata, WorldRecord } from '../types'
import {
AppComponents,
IPermissionChecker,
IWorldsManager,
Permissions,
WorldMetadata,
WorldRecord,
ContributorDomain
} from '../types'
import { streamToBuffer } from '@dcl/catalyst-storage'
import { Entity, EthAddress } from '@dcl/schemas'
import SQL from 'sql-template-strings'
Expand Down Expand Up @@ -181,6 +189,26 @@ export async function createWorldsManagerComponent({
await database.query(sql)
}

async function getContributableDomains(address: string): Promise<{ domains: ContributorDomain[]; count: number }> {
const result = await database.query<ContributorDomain>(SQL`
SELECT DISTINCT name, array_agg(permission) as user_permissions, size, owner
FROM (
SELECT *
FROM worlds w, json_each_text(w.permissions) AS perm(permission, permissionValue)
WHERE permission = ANY(ARRAY['deployment', 'streaming'])
) AS wp
WHERE EXISTS (
SELECT 1 FROM json_array_elements_text(wp.permissionValue::json -> 'wallets') as wallet WHERE LOWER(wallet) = LOWER(${address})
)
GROUP BY name, size, owner
`)

return {
domains: result.rows,
count: result.rowCount
}
}

return {
getRawWorldRecords,
getDeployedWorldCount,
Expand All @@ -190,6 +218,7 @@ export async function createWorldsManagerComponent({
deployScene,
storePermissions,
permissionCheckerForWorld,
undeploy
undeploy,
getContributableDomains
}
}
16 changes: 16 additions & 0 deletions src/controllers/handlers/contributor-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DecentralandSignatureContext } from '@dcl/platform-crypto-middleware'
import { HandlerContextWithPath } from '../../types'
import { IHttpServerComponent } from '@well-known-components/interfaces'

export async function getContributableDomainsHandler(
ctx: HandlerContextWithPath<'worldsManager', '/world/contribute'> & DecentralandSignatureContext<any>
): Promise<IHttpServerComponent.IResponse> {
const { worldsManager } = ctx.components
const address = ctx.verification!.auth
const body = await worldsManager.getContributableDomains(address)

return {
status: 200,
body
}
}
3 changes: 3 additions & 0 deletions src/controllers/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { undeployEntity } from './handlers/undeploy-entity-handler'
import { bearerTokenMiddleware, errorHandler } from '@dcl/platform-server-commons'
import { reprocessABHandler } from './handlers/reprocess-ab-handler'
import { garbageCollectionHandler } from './handlers/garbage-collection'
import { getContributableDomainsHandler } from './handlers/contributor-handler'

export async function setupRouter(globalContext: GlobalContext): Promise<Router<GlobalContext>> {
const router = new Router<GlobalContext>()
Expand All @@ -48,6 +49,8 @@ export async function setupRouter(globalContext: GlobalContext): Promise<Router<
router.head('/contents/:hashId', headContentFile)
router.get('/contents/:hashId', getContentFile)

router.get('/wallet/contribute', signedFetchMiddleware, getContributableDomainsHandler)

router.get('/world/:world_name/permissions', getPermissionsHandler)
router.post('/world/:world_name/permissions/:permission_name', signedFetchMiddleware, postPermissionsHandler)
router.put(
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export type IWorldsManager = {
storePermissions(worldName: string, permissions: Permissions): Promise<void>
permissionCheckerForWorld(worldName: string): Promise<IPermissionChecker>
undeploy(worldName: string): Promise<void>
getContributableDomains(address: string): Promise<{ domains: ContributorDomain[]; count: number }>
}

export type IPermissionsManager = {
Expand Down Expand Up @@ -367,3 +368,10 @@ export type WorldRecord = {
export type BlockedRecord = { wallet: string; created_at: Date; updated_at: Date }

export const TWO_DAYS_IN_MS = 2 * 24 * 60 * 60 * 1000

export type ContributorDomain = {
name: string
user_permissions: string[]
owner: string
size: string
}
139 changes: 139 additions & 0 deletions test/integration/contributor-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Authenticator } from '@dcl/crypto'
import { test } from '../components'
import { IWorldCreator, IWorldsManager, Permissions, PermissionType } from '../../src/types'
import { defaultPermissions } from '../../src/logic/permissions-checker'
import { Identity, getIdentity, getAuthHeaders } from '../utils'

test('ContributorHandler', function ({ components }) {
let worldCreator: IWorldCreator
let worldsManager: IWorldsManager
let identity: Identity
let worldName: string
let owner: string

function makeRequest(path: string, identity: Identity) {
return components.localFetch.fetch(path, {
method: 'GET',
headers: {
...getAuthHeaders(
'get',
path,
{
origin: 'https://play.decentraland.org',
intent: 'dcl:explorer:comms-handshake',
signer: 'dcl:explorer',
isGuest: 'false'
},
(payload) =>
Authenticator.signPayload(
{
ephemeralIdentity: identity.ephemeralIdentity,
expiration: new Date(),
authChain: identity.authChain.authChain
},
payload
)
)
}
})
}

beforeEach(async () => {
worldCreator = components.worldCreator
worldsManager = components.worldsManager

identity = await getIdentity()

const created = await worldCreator.createWorldWithScene({ owner: identity.authChain })
worldName = created.worldName
owner = created.owner.authChain[0].payload.toLowerCase()
})

describe('/wallet/contribute', () => {
describe("when user doesn't have contributor permission to any world", () => {
it('returns an empty list', async () => {
const r = await makeRequest('/wallet/contribute', identity)

expect(r.status).toBe(200)
expect(await r.json()).toMatchObject({ domains: [], count: 0 })
})
})

describe('when user has streamer permission to world', () => {
it('returns list of domains', async () => {
const permissions: Permissions = {
...defaultPermissions(),
streaming: {
type: PermissionType.AllowList,
wallets: [identity.realAccount.address]
}
}
await worldsManager.storePermissions(worldName, permissions)
const r = await makeRequest('/wallet/contribute', identity)

expect(r.status).toBe(200)
expect(await r.json()).toMatchObject({
domains: [
{
name: worldName,
user_permissions: ['streaming'],
owner,
size: '0'
}
],
count: 1
})
})
})

describe('when user has deployment permission to world', () => {
it('returns list of domains', async () => {
const permissions: Permissions = {
...defaultPermissions(),
deployment: {
type: PermissionType.AllowList,
wallets: [identity.realAccount.address]
}
}

await worldsManager.storePermissions(worldName, permissions)
const r = await makeRequest('/wallet/contribute', identity)

expect(r.status).toBe(200)
const result = await r.json()
expect(result).toMatchObject({
domains: [
{
name: worldName,
user_permissions: ['deployment'],
owner,
size: '0'
}
],
count: 1
})
})
})

describe('when world streaming permission is unrestricted', () => {
it('return empty list', async () => {
const permissions: Permissions = {
...defaultPermissions(),
streaming: {
type: PermissionType.Unrestricted
}
}

await worldsManager.storePermissions(worldName, permissions)
const r = await makeRequest('/wallet/contribute', identity)

expect(r.status).toBe(200)
const result = await r.json()
expect(result).toMatchObject({
domains: [],
count: 0
})
})
})
})
})
29 changes: 29 additions & 0 deletions test/mocks/worlds-manager-mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AppComponents,
ContributorDomain,
IPermissionChecker,
IWorldsManager,
Permissions,
Expand Down Expand Up @@ -121,7 +122,35 @@ export async function createWorldsManagerMockComponent({
await storage.delete([`name-${worldName.toLowerCase()}`])
}

async function getContributableDomains(address: string): Promise<{ domains: ContributorDomain[]; count: number }> {
const domains: ContributorDomain[] = []
for await (const name of storage.allFileIds('name-')) {
const metadata = await getMetadataForWorld(name)
const entity = await getEntityForWorld(name)
if (entity) {
const content = await storage.retrieve(`${entity.id}.auth`)
const authChain = JSON.parse((await streamToBuffer(await content?.asStream())).toString())
const hasDeploymentPermission = metadata.permissions.deployment.wallets.includes(address)
const hasStreamingPermission =
'wallets' in metadata.permissions.streaming && metadata.permissions.streaming.wallets.includes(address)
if (hasStreamingPermission || hasStreamingPermission) {
domains.push({
name,
user_permissions: [
...(hasDeploymentPermission ? ['deployment'] : []),
...(hasStreamingPermission ? ['streaming'] : [])
],
size: '0',
owner: authChain[0].payload
})
}
}
}
return { domains, count: domains.length }
}

return {
getContributableDomains,
getRawWorldRecords,
getDeployedWorldCount,
getDeployedWorldEntities,
Expand Down

0 comments on commit 694527f

Please sign in to comment.