1+ import * as readline from 'readline'
12import { ChainId , Scene } from '@dcl/schemas'
3+ import { AuthChain } from '@dcl/crypto'
24import { Lifecycle } from '@well-known-components/interfaces'
35import { createCatalystClient , createContentClient , CatalystClient , ContentClient } from 'dcl-catalyst-client'
46import { getCatalystServersFromCache } from 'dcl-catalyst-client/dist/contracts-snapshots'
@@ -67,6 +69,44 @@ export async function getCatalyst(
6769 }
6870}
6971
72+ export function buildDeleteScenesFromWorldPayload ( worldName : string ) : string {
73+ const encodedName = encodeURIComponent ( worldName )
74+ const path = `/entities/${ encodedName } `
75+ const timestamp = String ( Date . now ( ) )
76+ const metadata = '{}'
77+ return [ 'delete' , path , timestamp , metadata ] . join ( ':' ) . toLowerCase ( )
78+ }
79+
80+ export async function deleteWorldScenes (
81+ components : Pick < CliComponents , 'logger' > ,
82+ worldName : string ,
83+ deleteSignature : string ,
84+ targetContent ?: string
85+ ) : Promise < Response > {
86+ const { logger } = components
87+
88+ const encodedName = encodeURIComponent ( worldName )
89+ const deleteUrl = `${ targetContent } /entities/${ encodedName } `
90+
91+ const authChain : AuthChain = JSON . parse ( deleteSignature )
92+ const lastLink = authChain [ authChain . length - 1 ]
93+ const payloadParts = lastLink . payload . split ( ':' )
94+ const timestamp = payloadParts [ 2 ] || String ( Date . now ( ) )
95+ const metadata = payloadParts [ 3 ] || '{}'
96+
97+ const headers : Record < string , string > = {
98+ 'x-identity-timestamp' : timestamp ,
99+ 'x-identity-metadata' : metadata
100+ }
101+ authChain . forEach ( ( link , i ) => {
102+ headers [ `x-identity-auth-chain-${ i } ` ] = JSON . stringify ( link )
103+ } )
104+
105+ const response = await fetch ( deleteUrl , { method : 'DELETE' , headers } )
106+ logger . info ( `[DELETE] deleting world scenes status=${ response . status } ` )
107+ return response
108+ }
109+
70110export async function getAddressAndSignature (
71111 components : CliComponents ,
72112 awaitResponse : IFuture < void > ,
@@ -75,7 +115,8 @@ export async function getAddressAndSignature(
75115 files : IFile [ ] ,
76116 skipValidations : boolean ,
77117 linkOptions : Omit < dAppOptions , 'uri' > ,
78- deployCallback : ( response : LinkerResponse ) => Promise < void >
118+ deployCallback : ( response : LinkerResponse ) => Promise < void > ,
119+ deleteScenesFromWorldPayload ?: string
79120) : Promise < { program ?: Lifecycle . ComponentBasedProgram < unknown > } > {
80121 if ( process . env . DCL_PRIVATE_KEY ) {
81122 const wallet = createWallet ( process . env . DCL_PRIVATE_KEY )
@@ -84,13 +125,13 @@ export async function getAddressAndSignature(
84125 wallet . address ,
85126 ethSign ( hexToBytes ( wallet . privateKey ) , messageToSign )
86127 )
87- const linkerResponse = { authChain, address : wallet . address }
128+ const linkerResponse : LinkerResponse = { authChain, address : wallet . address }
88129 await deployCallback ( linkerResponse )
89130 awaitResponse . resolve ( )
90131 return { }
91132 }
92133
93- const sceneInfo = await getSceneInfo ( components , scene , messageToSign , skipValidations )
134+ const sceneInfo = await getSceneInfo ( components , scene , messageToSign , skipValidations , deleteScenesFromWorldPayload )
94135 const { router : commonRouter } = setRoutes ( components , sceneInfo )
95136 const router = setDeployRoutes ( commonRouter , components , awaitResponse , sceneInfo , files , deployCallback )
96137
@@ -109,7 +150,6 @@ function setDeployRoutes(
109150) : Router < object > {
110151 const { logger } = components
111152
112- // We need to wait so the linker-dapp can receive the response and show a nice message.
113153 const resolveLinkerPromise = ( ) => setTimeout ( ( ) => awaitResponse . resolve ( ) , 100 )
114154 const rejectLinkerPromise = ( e : Error ) => setTimeout ( ( ) => awaitResponse . reject ( e ) , 100 )
115155 let linkerResponse : LinkerResponse
@@ -129,8 +169,6 @@ function setDeployRoutes(
129169 const value = await getPointers ( components , pointer , network )
130170 const deployedToAll = new Set ( value . map ( ( c ) => c . entityId ) ) . size === 1
131171
132- // Deployed to every catalyst, close the linker dapp and
133- // exit the command automatically so the user dont have to.
134172 if ( deployedToAll ) resolveLinkerPromise ( )
135173
136174 return {
@@ -140,21 +178,17 @@ function setDeployRoutes(
140178
141179 router . post ( '/api/deploy' , async ( ctx ) => {
142180 const value = ( await ctx . request . json ( ) ) as LinkerResponse
143-
144181 if ( ! value . address || ! value . authChain || ! value . chainId ) {
145182 const errorMessage = `Invalid payload: ${ Object . keys ( value ) . join ( ' - ' ) } `
146183 logger . error ( errorMessage )
147184 resolveLinkerPromise ( )
148185 return { status : 400 , body : { message : errorMessage } }
149186 }
150187
151- // Store the chainId so we can use it on the catalyst pointers.
152188 linkerResponse = value
153189
154190 try {
155191 await deployCallback ( value )
156- // If its a world we dont wait for the catalyst pointers.
157- // Close the program.
158192 if ( sceneInfo . isWorld ) {
159193 resolveLinkerPromise ( )
160194 }
@@ -185,13 +219,15 @@ export interface SceneInfo {
185219 isPortableExperience : boolean
186220 isWorld : boolean
187221 world ?: string
222+ deleteScenesFromWorldPayload ?: string
188223}
189224
190225export async function getSceneInfo (
191226 components : Pick < CliComponents , 'config' > ,
192227 scene : Scene ,
193228 rootCID : string ,
194- skipValidations : boolean
229+ skipValidations : boolean ,
230+ deleteScenesFromWorldPayload ?: string
195231) {
196232 const {
197233 scene : { parcels, base } ,
@@ -211,6 +247,54 @@ export async function getSceneInfo(
211247 skipValidations,
212248 isPortableExperience : ! ! isPortableExperience ,
213249 isWorld : sceneHasWorldCfg ( scene ) ,
214- world : scene . worldConfiguration ?. name
250+ world : scene . worldConfiguration ?. name ,
251+ deleteScenesFromWorldPayload
215252 }
216253}
254+
255+ export interface WorldScene {
256+ entityId : string
257+ parcels ?: string [ ]
258+ entity ?: { metadata ?: { display ?: { title ?: string } } }
259+ }
260+
261+ export function getScenesOnOtherParcels ( existingScenes : WorldScene [ ] , deployingParcels : string [ ] ) : WorldScene [ ] {
262+ const parcelsSet = new Set ( deployingParcels )
263+ return existingScenes . filter ( ( scene ) => {
264+ const sceneParcels = scene . parcels || [ ]
265+ return sceneParcels . every ( ( p ) => ! parcelsSet . has ( p ) )
266+ } )
267+ }
268+
269+ interface WorldScenesResponse {
270+ scenes : WorldScene [ ]
271+ total : number
272+ }
273+
274+ export async function fetchWorldScenes (
275+ logger : { info : ( msg : string ) => void } ,
276+ worldName : string ,
277+ targetContent ?: string
278+ ) : Promise < WorldScene [ ] > {
279+ const encodedName = encodeURIComponent ( worldName )
280+ const url = `${ targetContent } /world/${ encodedName } /scenes`
281+ const response = await fetch ( url )
282+ if ( ! response . ok ) {
283+ const text = await response . text ( )
284+ logger . info ( `[DEPLOY] fetching scenes from world - error: ${ text } ` )
285+ throw new Error ( `Failed to fetch world scenes: ${ response . status } ${ response . statusText } ` )
286+ }
287+ const data = ( await response . json ( ) ) as WorldScenesResponse
288+ logger . info ( `[DEPLOY] fetching scenes from world success: total=${ data . total } , scenes=${ data . scenes ?. length } ` )
289+ return data . scenes || [ ]
290+ }
291+
292+ export function promptUser ( question : string ) : Promise < boolean > {
293+ const rl = readline . createInterface ( { input : process . stdin , output : process . stdout } )
294+ return new Promise ( ( resolve ) => {
295+ rl . question ( question , ( answer ) => {
296+ rl . close ( )
297+ resolve ( answer . toLowerCase ( ) === 'y' || answer . toLowerCase ( ) === 'yes' )
298+ } )
299+ } )
300+ }
0 commit comments