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: add contributable domains endpoint #294

Merged
merged 7 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
32 changes: 30 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,25 @@ 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)
) 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 +217,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
}
166 changes: 166 additions & 0 deletions test/integration/contributor-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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 access permission to world', () => {
it('returns list of domains', async () => {
const permissions: Permissions = {
...defaultPermissions(),
access: {
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: ['access'],
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
Loading