@@ -21,6 +21,19 @@ import type {
2121 SceneDeployerInfo
2222} from './places.types'
2323
24+ // Custom RTK Query error status used by getSceneMetadata to signal that an ENS
25+ // realm does not resolve to a real World on the Worlds Content Server. Consumers
26+ // (PlacesPage) match on it via isWorldNotFoundError to redirect to
27+ // /jump/places/invalid.
28+ const WORLD_NOT_FOUND = 'WORLD_NOT_FOUND'
29+
30+ // Narrow a getSceneMetadata error to the WORLD_NOT_FOUND signal. Keeps the
31+ // producer and consumer matching on one shape instead of a structural cast at
32+ // the call site, so the contract stays in a single place.
33+ function isWorldNotFoundError ( error : unknown ) : boolean {
34+ return typeof error === 'object' && error !== null && ( error as { status ?: unknown } ) . status === WORLD_NOT_FOUND
35+ }
36+
2437function resolveIdentity ( address : string | undefined ) {
2538 // Resolving the identity inside queryFn (instead of accepting it as a query
2639 // arg) keeps the ephemeral key material out of `state.placesClient.queries.*.
@@ -82,6 +95,27 @@ async function fetchPeerDeployment(peerUrl: string, entityId: string): Promise<P
8295 return body . deployments ?. [ 0 ] ?? null
8396}
8497
98+ // Worlds live on the Worlds Content Server, which exposes the active scene
99+ // entity (carrying `metadata.owner`) under `/entities/active` keyed by the world
100+ // name — note the path has no `/content` prefix, unlike the main Catalyst's
101+ // `fetchPeerSceneEntity`.
102+ //
103+ // A `null` return is meaningful: it means the server answered 200 with no
104+ // active entity, i.e. the world does not exist on the WCS. A non-OK response
105+ // (outage, 5xx) THROWS instead, so callers can tell "world doesn't exist" apart
106+ // from "couldn't reach the server" and avoid treating a transient outage as a
107+ // missing world.
108+ async function fetchWorldSceneEntity ( worldsUrl : string , worldName : string ) : Promise < PeerSceneEntity | null > {
109+ const response = await fetch ( `${ worldsUrl } /entities/active` , {
110+ method : 'POST' ,
111+ headers : { Accept : '*/*' , 'Content-Type' : 'application/json' } ,
112+ body : JSON . stringify ( { pointers : [ worldName ] } )
113+ } )
114+ if ( ! response . ok ) throw new Error ( `Worlds Content Server responded ${ response . status } ` )
115+ const entities : PeerSceneEntity [ ] = await response . json ( )
116+ return entities ?. [ 0 ] ?? null
117+ }
118+
85119async function fetchPeerProfile ( peerUrl : string , address : string ) : Promise < PeerProfile | null > {
86120 const response = await fetch ( `${ peerUrl } /lambdas/profiles` , {
87121 method : 'POST' ,
@@ -104,6 +138,23 @@ function toCreator(address: string, profile: PeerProfile | null): Creator | null
104138 }
105139}
106140
141+ // Resolve a Catalyst profile into the card's creator slot. Returns null when the
142+ // address has no profile name — curated scenes (e.g. Genesis Plaza) and some
143+ // world owners are deployed by / owned by addresses with no Catalyst user
144+ // profile, and returning null lets the Card fall back to the Places API's
145+ // contact_name instead of overriding it with a placeholder like "Unknown".
146+ async function resolveDeployerInfo ( peerUrl : string , address : string ) : Promise < SceneDeployerInfo | null > {
147+ const profile = await fetchPeerProfile ( peerUrl , address )
148+ const avatar = profile ?. avatars ?. [ 0 ]
149+ const deployerName = avatar ?. name || avatar ?. realName
150+ if ( ! deployerName ) return null
151+ return {
152+ deployerAddress : address ,
153+ deployerName,
154+ deployerAvatar : avatar ?. avatar ?. snapshots ?. face256
155+ }
156+ }
157+
107158const placesEndpoints = placesClient . injectEndpoints ( {
108159 endpoints : build => ( {
109160 getJumpPlaces : build . query < JumpPlace [ ] , GetPlacesArgs > ( {
@@ -171,37 +222,45 @@ const placesEndpoints = placesClient.injectEndpoints({
171222 providesTags : ( _result , _error , { id } ) => [ { type : 'JumpEvent' , id } ]
172223 } ) ,
173224 getSceneMetadata : build . query < SceneDeployerInfo | null , GetSceneMetadataArgs > ( {
174- queryFn : async ( { position } ) => {
225+ queryFn : async ( { position, realm } ) => {
175226 try {
176227 const peerUrl = getEnv ( 'PEER_URL' )
177228 if ( ! peerUrl ) throw new Error ( 'PEER_URL is not set' )
178229
230+ // Worlds aren't on the main Catalyst at `position` — their scene lives
231+ // on the Worlds Content Server keyed by the world name. Resolve the
232+ // owner from the scene entity's `metadata.owner` and look that profile
233+ // up on the Catalyst lambdas.
234+ if ( realm && isEns ( realm ) ) {
235+ const worldsUrl = getEnv ( 'WORLDS_CONTENT_SERVER_URL' )
236+ if ( ! worldsUrl ) throw new Error ( 'WORLDS_CONTENT_SERVER_URL is not set' )
237+
238+ const entity = await fetchWorldSceneEntity ( worldsUrl , realm . toLowerCase ( ) )
239+ // No active scene entity means the realm is not a real World on the
240+ // WCS — even when the Places API still serves a stale record for it.
241+ // Surface a typed not-found so the page can redirect to
242+ // /jump/places/invalid; this is distinct from a FETCH_ERROR raised on
243+ // a WCS outage (fetchWorldSceneEntity throws), which must NOT redirect.
244+ if ( ! entity ) return { error : { status : WORLD_NOT_FOUND } }
245+
246+ const ownerAddress = entity . metadata ?. owner
247+ if ( ! ownerAddress ) return { data : null }
248+
249+ return { data : await resolveDeployerInfo ( peerUrl , ownerAddress ) }
250+ }
251+
179252 const entity = await fetchPeerSceneEntity ( peerUrl , position )
180253 if ( ! entity ) return { data : null }
181254
182255 const deployment = await fetchPeerDeployment ( peerUrl , entity . id )
183256 if ( ! deployment ) return { data : null }
184257
185- const profile = await fetchPeerProfile ( peerUrl , deployment . deployedBy )
186- const avatar = profile ?. avatars ?. [ 0 ]
187- const deployerName = avatar ?. name || avatar ?. realName
188- // Curated scenes (e.g. Genesis Plaza) are deployed by addresses that
189- // have no Catalyst user profile. Returning null here lets the Card
190- // fall back to the Places API's contact_name instead of overriding
191- // it with a placeholder like "Unknown".
192- if ( ! deployerName ) return { data : null }
193-
194- const info : SceneDeployerInfo = {
195- deployerAddress : deployment . deployedBy ,
196- deployerName,
197- deployerAvatar : avatar ?. avatar ?. snapshots ?. face256
198- }
199- return { data : info }
258+ return { data : await resolveDeployerInfo ( peerUrl , deployment . deployedBy ) }
200259 } catch ( error ) {
201260 return { error : { status : 'FETCH_ERROR' , error : error instanceof Error ? error . message : 'Unknown error' } }
202261 }
203262 } ,
204- providesTags : ( _result , _err , args ) => [ { type : 'SceneMetadata' , id : args . position } ]
263+ providesTags : ( _result , _err , args ) => [ { type : 'SceneMetadata' , id : args . realm ? args . realm . toLowerCase ( ) : args . position } ]
205264 } ) ,
206265 getProfileCreator : build . query < Creator | null , { address : string } > ( {
207266 queryFn : async ( { address } ) => {
@@ -225,6 +284,7 @@ const { useGetJumpEventByIdQuery, useGetJumpEventsQuery, useGetJumpPlacesQuery,
225284 placesEndpoints
226285
227286export {
287+ isWorldNotFoundError ,
228288 placesEndpoints ,
229289 useGetJumpEventByIdQuery ,
230290 useGetJumpEventsQuery ,
0 commit comments