Skip to content

Commit cf3fbb9

Browse files
authored
feat: Unpublish world (#2946)
* feat: WIP unpublish world * feat: Add decentraland-crypto-fetch * feat: Support both unpublishing normal and world deployments * fix: Add tests * fix: Unskip tests * fix: Tests * fix: Tests
1 parent 101b1c8 commit cf3fbb9

File tree

5 files changed

+178
-54
lines changed

5 files changed

+178
-54
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"dcl-scene-writer": "^1.1.2",
3535
"decentraland": "^3.3.0",
3636
"decentraland-builder-scripts": "^0.24.0",
37+
"decentraland-crypto-fetch": "^2.0.1",
3738
"decentraland-dapps": "^16.10.0",
3839
"decentraland-ecs": "^6.6.1-20201020183014.commit-bdc29ef-hotfix",
3940
"decentraland-experiments": "^1.0.2",

src/lib/urn.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ describe('when decoding an URN', () => {
163163

164164
describe('when a valid entity urn is used', () => {
165165
describe('and the URN is an entity with a baseUrl', () => {
166-
it.only('should decode and return each group', () => {
166+
it('should decode and return each group', () => {
167167
expect(decodeURN('urn:decentraland:entity:anEntityId?=&baseUrl=https://aContentServerUrl')).toEqual({
168168
type: URNType.ENTITY,
169169
suffix: 'anEntityId?=&baseUrl=https://aContentServerUrl',

src/modules/deployment/sagas.spec.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { CatalystClient, createCatalystClient, createContentClient } from 'dcl-catalyst-client'
22
import { expectSaga } from 'redux-saga-test-plan'
33
import * as matchers from 'redux-saga-test-plan/matchers'
4+
import { call, select } from 'redux-saga/effects'
5+
import { AuthIdentity } from '@dcl/crypto'
46
import { buildEntity } from 'dcl-catalyst-client/dist/client/utils/DeploymentBuilder'
57
import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors'
8+
import { DataByKey } from 'decentraland-dapps/dist/lib/types'
9+
import cryptoFetch from 'decentraland-crypto-fetch'
10+
import { config } from 'config'
611
import { BuilderAPI } from 'lib/api/builder'
712
import { getCatalystContentUrl } from 'lib/api/peer'
813
import { isLoggedIn } from 'modules/identity/selectors'
@@ -11,9 +16,17 @@ import { getMedia } from 'modules/media/selectors'
1116
import { objectURLToBlob } from 'modules/media/utils'
1217
import { getName } from 'modules/profile/selectors'
1318
import { getData } from 'modules/project/selectors'
19+
import { getData as getDeployments } from 'modules/deployment/selectors'
1420
import { createFiles } from 'modules/project/export'
1521
import { getSceneByProjectId } from 'modules/scene/utils'
16-
import { deployToWorldRequest, fetchWorldDeploymentsRequest, fetchWorldDeploymentsSuccess } from './actions'
22+
import {
23+
clearDeploymentFailure,
24+
clearDeploymentRequest,
25+
clearDeploymentSuccess,
26+
deployToWorldRequest,
27+
fetchWorldDeploymentsRequest,
28+
fetchWorldDeploymentsSuccess
29+
} from './actions'
1730
import { deploymentSaga } from './sagas'
1831
import { makeContentFiles } from './contentUtils'
1932
import { Deployment } from './types'
@@ -148,3 +161,106 @@ describe('when handling fetch worlds deployments request', () => {
148161
.silentRun()
149162
})
150163
})
164+
165+
describe('when handling the clear deployment request action', () => {
166+
let deploymentId: string
167+
let identity: AuthIdentity | null
168+
let deployments: DataByKey<Deployment>
169+
let crytoFetchResponse: Response
170+
171+
beforeEach(() => {
172+
deploymentId = 'deploymentId'
173+
identity = null
174+
deployments = {}
175+
crytoFetchResponse = { ok: false, status: 500 } as Response
176+
})
177+
178+
describe('when the stored deployments does not contain a deployment for the provided id', () => {
179+
it('should put a clear deployment failure action signaling that the deployment id is invalid', async () => {
180+
await expectSaga(deploymentSaga, builderAPI, catalystClient)
181+
.provide([[select(getDeployments), deployments]])
182+
.put(clearDeploymentFailure(deploymentId, 'Unable to clear deployment: Invalid deployment'))
183+
.dispatch(clearDeploymentRequest(deploymentId))
184+
.silentRun()
185+
})
186+
})
187+
188+
describe('when the stored deployments does contain a deployment for the provided id', () => {
189+
beforeEach(() => {
190+
deployments[deploymentId] = {} as Deployment
191+
})
192+
193+
describe('when getting the identity returns a null or undefined value', () => {
194+
it('should put a clear deployment failure action signaling that the identity cannot be obtained', async () => {
195+
await expectSaga(deploymentSaga, builderAPI, catalystClient)
196+
.provide([
197+
[select(getDeployments), deployments],
198+
[call(getIdentity), identity]
199+
])
200+
.put(clearDeploymentFailure(deploymentId, 'Unable to clear deployment: Invalid identity'))
201+
.dispatch(clearDeploymentRequest(deploymentId))
202+
.silentRun()
203+
})
204+
})
205+
206+
describe('when getting the identity returns an identity', () => {
207+
beforeEach(() => {
208+
identity = {} as AuthIdentity
209+
})
210+
211+
describe('when the stored deployment is for a world', () => {
212+
let worldsContentServerUrl: string
213+
214+
beforeEach(() => {
215+
deployments[deploymentId].world = 'world'
216+
worldsContentServerUrl = 'https://worlds-content-server.com'
217+
jest.spyOn(config, 'get').mockReturnValueOnce(worldsContentServerUrl)
218+
})
219+
220+
describe('when the crypto fetch response is not ok', () => {
221+
it('should put a clear deployment failure action signaling that the response is not ok', async () => {
222+
await expectSaga(deploymentSaga, builderAPI, catalystClient)
223+
.provide([
224+
[select(getDeployments), deployments],
225+
[call(getIdentity), identity],
226+
[
227+
call(cryptoFetch, `${worldsContentServerUrl}/entities/world`, {
228+
method: 'DELETE',
229+
identity: identity!
230+
}),
231+
crytoFetchResponse
232+
]
233+
])
234+
.put(clearDeploymentFailure(deploymentId, `Unable to clear deployment: Response is not ok, status 500`))
235+
.dispatch(clearDeploymentRequest(deploymentId))
236+
.silentRun()
237+
})
238+
})
239+
240+
describe('when the crypto fetch response is ok', () => {
241+
beforeEach(() => {
242+
crytoFetchResponse = { ok: true } as Response
243+
})
244+
245+
it('should put a clear deployment success action signaling that the clear deployment executed successfuly', async () => {
246+
await expectSaga(deploymentSaga, builderAPI, catalystClient)
247+
.provide([
248+
[select(getDeployments), deployments],
249+
[call(getIdentity), identity],
250+
[
251+
call(cryptoFetch, `${worldsContentServerUrl}/entities/world`, {
252+
method: 'DELETE',
253+
identity: identity!
254+
}),
255+
crytoFetchResponse
256+
]
257+
])
258+
.put(clearDeploymentSuccess(deploymentId))
259+
.dispatch(clearDeploymentRequest(deploymentId))
260+
.silentRun()
261+
})
262+
})
263+
})
264+
})
265+
})
266+
})

src/modules/deployment/sagas.ts

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { createFetchComponent } from '@well-known-components/fetch-component'
12
import { CatalystClient, ContentClient, createContentClient } from 'dcl-catalyst-client'
23
import { Authenticator, AuthIdentity } from '@dcl/crypto'
34
import { Entity, EntityType } from '@dcl/schemas'
4-
import { createFetchComponent } from '@well-known-components/fetch-component'
5+
import cryptoFetch from 'decentraland-crypto-fetch'
56
import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors'
67
import { buildEntity } from 'dcl-catalyst-client/dist/client/utils/DeploymentBuilder'
78
import { takeLatest, put, select, call, take, all } from 'redux-saga/effects'
89
import { config } from 'config'
910
import { BuilderAPI, getEmptySceneUrl, getPreviewUrl } from 'lib/api/builder'
10-
import { getData as getDeployments } from 'modules/deployment/selectors'
1111
import { Deployment, SceneDefinition, Placement } from 'modules/deployment/types'
1212
import { takeScreenshot } from 'modules/editor/actions'
1313
import { fetchENSWorldStatusRequest } from 'modules/ens/actions'
@@ -24,6 +24,7 @@ import { Media } from 'modules/media/types'
2424
import { getName } from 'modules/profile/selectors'
2525
import { createFiles, EXPORT_PATH } from 'modules/project/export'
2626
import { getCurrentProject, getData as getProjects } from 'modules/project/selectors'
27+
import { getData as getDeployments } from 'modules/deployment/selectors'
2728
import { Project } from 'modules/project/types'
2829
import { getSceneByProjectId } from 'modules/scene/utils'
2930
import { Scene } from 'modules/scene/types'
@@ -59,9 +60,11 @@ import {
5960
fetchWorldDeploymentsRequest
6061
} from './actions'
6162
import { makeContentFiles } from './contentUtils'
62-
import { getEmptyDeployment, getThumbnail, UNPUBLISHED_PROJECT_ID } from './utils'
63+
import { UNPUBLISHED_PROJECT_ID, getEmptyDeployment, getThumbnail } from './utils'
6364
import { ProgressStage } from './types'
6465

66+
const getWorldsContentServerUrl = () => config.get('WORLDS_CONTENT_SERVER', '')
67+
6568
type UnwrapPromise<T> = T extends PromiseLike<infer U> ? U : T
6669

6770
// TODO: Remove this. This is using the store directly which it shouldn't and causes a circular dependency.
@@ -330,7 +333,7 @@ export function* deploymentSaga(builder: BuilderAPI, catalystClient: CatalystCli
330333
function* handleDeployToWorldRequest(action: DeployToWorldRequestAction) {
331334
const { world, projectId } = action.payload
332335
const contentClient = createContentClient({
333-
url: config.get('WORLDS_CONTENT_SERVER', ''),
336+
url: getWorldsContentServerUrl(),
334337
fetcher: createFetchComponent()
335338
})
336339
try {
@@ -364,57 +367,60 @@ export function* deploymentSaga(builder: BuilderAPI, catalystClient: CatalystCli
364367
function* handleClearDeploymentRequest(action: ClearDeploymentRequestAction) {
365368
const { deploymentId } = action.payload
366369

367-
const deployments: ReturnType<typeof getDeployments> = yield select(getDeployments)
368-
const deployment = deployments[deploymentId]
369-
if (!deployment) {
370-
yield put(deployToLandFailure('Unable to Publish: Invalid deployment'))
371-
return
372-
}
370+
try {
371+
const deployments: ReturnType<typeof getDeployments> = yield select(getDeployments)
372+
const deployment = deployments[deploymentId]
373373

374-
let contentClient: ContentClient
375-
if (deployment.world) {
376-
contentClient = createContentClient({
377-
url: config.get('WORLDS_CONTENT_SERVER', ''),
378-
fetcher: createFetchComponent()
379-
})
380-
} else {
381-
contentClient = yield call([catalystClient, 'getContentClient'])
382-
}
374+
if (!deployment) {
375+
throw new Error('Unable to clear deployment: Invalid deployment')
376+
}
383377

384-
const identity: AuthIdentity = yield getIdentity()
385-
if (!identity) {
386-
yield put(deployToLandFailure('Unable to Publish: Invalid identity'))
387-
return
388-
}
378+
const identity: AuthIdentity = yield call(getIdentity)
379+
380+
if (!identity) {
381+
throw new Error('Unable to clear deployment: Invalid identity')
382+
}
383+
384+
if (deployment.world) {
385+
const response: Response = yield call(cryptoFetch, `${getWorldsContentServerUrl()}/entities/${deployment.world}`, {
386+
method: 'DELETE',
387+
identity
388+
})
389+
390+
if (!response.ok) {
391+
throw new Error(`Unable to clear deployment: Response is not ok, status ${response.status}`)
392+
}
393+
} else {
394+
const contentClient: ContentClient = yield call([catalystClient, 'getContentClient'])
395+
const { placement } = deployment
396+
const [emptyProject, emptyScene] = getEmptyDeployment(deployment.projectId || UNPUBLISHED_PROJECT_ID)
397+
const files: UnwrapPromise<ReturnType<typeof createFiles>> = yield call(createFiles, {
398+
project: emptyProject,
399+
scene: emptyScene,
400+
point: placement.point,
401+
rotation: placement.rotation,
402+
thumbnail: getEmptySceneUrl(),
403+
author: null,
404+
isDeploy: true,
405+
isEmpty: true,
406+
onProgress: handleProgress(ProgressStage.CREATE_FILES),
407+
world: deployment.world ?? undefined
408+
})
409+
const contentFiles: Map<string, Buffer> = yield call(makeContentFiles, files)
410+
const sceneDefinition: SceneDefinition = JSON.parse(files[EXPORT_PATH.SCENE_FILE])
411+
const { entityId, files: hashedFiles } = yield call(buildEntity, {
412+
type: EntityType.SCENE,
413+
pointers: [...sceneDefinition.scene.parcels],
414+
metadata: sceneDefinition,
415+
files: contentFiles
416+
})
417+
const authChain = Authenticator.signPayload(identity, entityId)
418+
yield call([contentClient, 'deploy'], { entityId, files: hashedFiles, authChain })
419+
}
389420

390-
try {
391-
const { placement } = deployment
392-
const [emptyProject, emptyScene] = getEmptyDeployment(deployment.projectId || UNPUBLISHED_PROJECT_ID)
393-
const files: UnwrapPromise<ReturnType<typeof createFiles>> = yield call(createFiles, {
394-
project: emptyProject,
395-
scene: emptyScene,
396-
point: placement.point,
397-
rotation: placement.rotation,
398-
thumbnail: getEmptySceneUrl(),
399-
author: null,
400-
isDeploy: true,
401-
isEmpty: true,
402-
onProgress: handleProgress(ProgressStage.CREATE_FILES),
403-
world: deployment.world ?? undefined
404-
})
405-
const contentFiles: Map<string, Buffer> = yield call(makeContentFiles, files)
406-
const sceneDefinition: SceneDefinition = JSON.parse(files[EXPORT_PATH.SCENE_FILE])
407-
const { entityId, files: hashedFiles } = yield call(buildEntity, {
408-
type: EntityType.SCENE,
409-
pointers: [...sceneDefinition.scene.parcels],
410-
metadata: sceneDefinition,
411-
files: contentFiles
412-
})
413-
const authChain = Authenticator.signPayload(identity, entityId)
414-
yield call([contentClient, 'deploy'], { entityId, files: hashedFiles, authChain })
415421
yield put(clearDeploymentSuccess(deploymentId))
416-
} catch (error) {
417-
yield put(clearDeploymentFailure(deploymentId, error.message))
422+
} catch (e) {
423+
yield put(clearDeploymentFailure(deploymentId, e.message))
418424
}
419425
}
420426

@@ -502,7 +508,7 @@ export function* deploymentSaga(builder: BuilderAPI, catalystClient: CatalystCli
502508

503509
function* handleFetchWorldDeploymentsRequest(action: FetchWorldDeploymentsRequestAction) {
504510
const { worlds } = action.payload
505-
const worldContentClient = createContentClient({ url: config.get('WORLDS_CONTENT_SERVER', ''), fetcher: createFetchComponent() })
511+
const worldContentClient = createContentClient({ url: getWorldsContentServerUrl(), fetcher: createFetchComponent() })
506512
try {
507513
const entities: Entity[] = []
508514

0 commit comments

Comments
 (0)