import { Client , start , healthChecker as smHealthChecker , } from "@substrate/smoldot-light" ; import { JsonRpcCallback , SmoldotHealth } from "@substrate/smoldot-light" ; import { ExposedAppInfo , App , ConnectionManagerInterface } from "./types" ; import EventEmitter from "eventemitter3" ; import { StateEmitter , State } from "./types" ; import { NetworkMainInfo , Network } from "../types" ; import { logger } from "@polkadot/util" ; import { ToExtension } from "@substrate/connect-extension-protocol" ; import westend from "../../public/assets/westend.json" ; import kusama from "../../public/assets/kusama.json" ; import polkadot from "../../public/assets/polkadot.json" ; import rococo from "../../public/assets/rococo.json" ; export const wellKnownChains = new Map < string , string > ( [ [ "polkadot" , JSON . stringify ( polkadot ) ] , [ "kusama" , JSON . stringify ( kusama ) ] , [ "rococo" , JSON . stringify ( rococo ) ] , [ "westend" , JSON . stringify ( westend ) ] , ] ) ; const l = logger ( "Extension Connection Manager" ) ; /**
 * ConnectionManager is the main class involved in managing connections from
 * apps.  It keeps track of apps and it is also responsible for triggering
 * events when the state changes for the UI to update accordingly.
 */ export class ConnectionManager extends ( EventEmitter as { new ( ) : StateEmitter ; } ) implements ConnectionManagerInterface { readonly #apps : App [ ] = [ ] ; #client : Client | undefined = undefined ; #networks : Network [ ] = [ ] ; smoldotLogLevel = 3 ; returns get registeredApps ( ) : string [ ] { return this . #apps . map ( ( a ) => a . name ) ; } returns get registeredNetworks ( ) : NetworkMainInfo [ ] { return this . #networks . map ( ( s ) => ( { name : s . name , id : s . id , status : s . status , } ) ) ; } returns get apps ( ) : ExposedAppInfo [ ] { return this . #apps . map ( ( a ) => ( { appName : a . appName , chainName : a . chainName , healthStatus : a . healthStatus , pendingRequests : a . pendingRequests , state : a . state , url : a . url , tabId : a . tabId , } ) ) ; } returns getState ( ) { const state = { apps : [ ] } ; return this . #apps . reduce ( ( result , app ) => { let a = result . apps . find ( ( a ) => a . name === app . appName && a . tabId === app . tabId ) ; if ( a === undefined ) { a = { name : app . appName , tabId : app . tabId , networks : [ ] , } ; result . apps . push ( a ) ; } a . networks . push ( { name : app . chainName } ) ; return result ; } , state ) ; } param tabId disconnectTab ( tabId ) { this . #apps . filter ( ( a ) => a . tabId && a . tabId === tabId ) . forEach ( ( a ) => { this . disconnect ( a ) ; } ) ; } /**
     * disconnectAll disconnects all apps connected for all tabs
     */ disconnectAll ( ) { this . #apps . filter ( ( a ) => a ) . forEach ( ( a ) => this . disconnect ( a ) ) ; } createApp ( incPort ) { const splitIdx = incPort . name . indexOf ( "::" ) ; if ( splitIdx === - 1 ) { const payload = `Invalid port name ${ incPort . name } expected <app_name>::<chain_name>` ; incPort . postMessage ( { type : "error" , payload } ) ; incPort . disconnect ( ) ; throw new Error ( payload ) ; } const { name , sender } = incPort ; const appName = name . substr ( 0 , splitIdx ) ; const chainName = name . substr ( splitIdx + 2 , name . length ) ; const tabId = sender ?. tab ?. id || - 1 ; const url = sender ?. url ; const port = incPort ; const state = "connected" ; const pendingRequests = [ ] ; const healthChecker = smHealthChecker ( ) ; const app = { appName , chainName , name , tabId , url , port , state , healthChecker , pendingRequests , } ; port . onMessage . addListener ( this . #handleMessage ) ; port . onDisconnect . addListener ( ( ) => { this . #handleDisconnect ( app ) ; } ) ; return app ; } param port addApp ( port ) { if ( ! this . #client ) { throw new Error ( "Smoldot client does not exist." ) ; } if ( this . #findApp ( port ) ) { port . postMessage ( { type : "error" , payload : `App ${ port . name } already exists.` , } ) ; port . disconnect ( ) ; return ; } try { const app = this . createApp ( port ) ; this . registerApp ( app ) ; } catch ( error ) { l . error ( `Error while adding chain: ${ error }` ) ; } } param app registerApp ( app ) { this . #apps . push ( app ) ; this . emit ( "stateChanged" , this . getState ( ) ) ; this . emit ( "appsChanged" , this . apps ) ; } param app unregisterApp ( app ) { const idx = this . #apps . findIndex ( ( a ) => a . name === app . name ) ; this . #apps . splice ( idx , 1 ) ; this . emit ( "stateChanged" , this . getState ( ) ) ; this . emit ( "appsChanged" , this . apps ) ; } /** shutdown shuts down the connected smoldot client. */ async shutdown ( ) { await this . #client ?. terminate ( ) ; this . #client = undefined ; } /**
     * initSmoldot initializes the smoldot client.
     */ initSmoldot ( ) { try { this . #client = start ( { maxLogLevel : this . smoldotLogLevel , } ) ; } catch ( err ) { l . error ( `Error while initializing smoldot: ${ err }` ) ; } } param spec param jsonRpcCallback param relayChain returns async addChain ( chainSpec , jsonRpcCallback ? , tabId ? ) { if ( ! this . #client ) { throw new Error ( "Smoldot client does not exist." ) ; } const { name , id , relay_chain } = JSON . parse ( chainSpec ) ; const potentialNetworks = relay_chain ? this . #networks . filter ( ( n ) => n . tabId === tabId ) : this . #networks ; const addedChain = await this . #client . addChain ( { chainSpec , jsonRpcCallback , potentialRelayChains : potentialNetworks . map ( ( r ) => r . chain ) , } ) ; const existingNetwork = this . #networks . find ( ( n ) => n . name . toLowerCase ( ) === name . toLowerCase ( ) && n . tabId === tabId ) ; const network = { tabId : tabId || 0 , id , name : name . toLowerCase ( ) , chain : addedChain , status : "connected" , } ; ! existingNetwork && this . #networks . push ( network ) ; return existingNetwork || network ; } #initHealthChecker = ( app , isParachain ? ) : void => { if ( isParachain ) { app . parachain && app . healthChecker ?. setSendJsonRpc ( app . parachain . sendJsonRpc ) ; } else { app . chain && app . healthChecker ?. setSendJsonRpc ( app . chain . sendJsonRpc ) ; } void app . healthChecker ?. start ( ( health ) => { if ( health && ( app . healthStatus ?. peers !== health . peers || app . healthStatus . isSyncing !== health . isSyncing ) ) { this . emit ( "appsChanged" , this . apps ) ; } app . healthStatus = health ; } ) ; if ( app . pendingRequests . length > 0 ) { app . pendingRequests . forEach ( ( req ) => app . healthChecker ?. sendJsonRpc ( req ) ) ; app . pendingRequests = [ ] ; } } ; #handleError = ( app , e ) : void => { app . port . postMessage ( { type : "error" , payload : e . message } ) ; app . port . disconnect ( ) ; this . unregisterApp ( app ) ; } ; /** Handles the incoming message that contains Spec. */ #handleSpecMessage = ( app , relayChainSpec , parachainSpec ? ) : void => { if ( ! relayChainSpec ) { const error = new Error ( "Relay chain spec was not found" ) ; this . #handleError ( app , error ) ; return ; } const rpcCallback = ( rpc ) => { const rpcResp = app . healthChecker ?. responsePassThrough ( rpc ) ; if ( rpcResp ) app . port . postMessage ( { type : "rpc" , payload : rpcResp } ) ; } ; let chainPromise ; if ( parachainSpec ) { chainPromise = this . addChain ( relayChainSpec , undefined , app . tabId ) . then ( ( network ) => { app . chain = network . chain ; return this . addChain ( parachainSpec , rpcCallback , app . tabId ) ; } ) . then ( ( network ) => { app . parachain = network . chain ; app . chainName = JSON . parse ( parachainSpec ) . name ; return ; } ) ; } else { chainPromise = this . addChain ( relayChainSpec , rpcCallback , app . tabId ) . then ( ( network ) => { app . chain = network . chain ; return ; } ) ; } chrome . storage . sync . get ( "notifications" , ( s ) => { s . notifications && chrome . notifications . create ( app . port . name , { title : "Substrate Connect" , message : `App ${ app . appName } connected to ${ app . chainName }.` , iconUrl : "./icons/icon-32.png" , type : "basic" , } ) ; } ) ; chainPromise . then ( ( ) => { this . #initHealthChecker ( app , ! ! app . parachain ) ; } ) . catch ( ( e ) => { this . #handleError ( app , e ) ; } ) ; } ; #findApp ( port ) { return this . #apps . find ( ( a ) => a . name === port . name && a . tabId === port . sender ?. tab ?. id ) ; } #handleMessage = ( msg , port ) : void => { if ( ( msg . type !== "rpc" && msg . type !== "add-chain" && msg . type !== "add-well-known-chain" ) || ! msg . payload ) { console . warn ( `Unrecognised message type '${ msg . type }' or payload '${ msg . payload }' received from content script` ) ; return ; } const app = this . #findApp ( port ) ; if ( ! app ) return ; if ( msg . type === "rpc" ) { if ( app . chain === undefined ) { app . pendingRequests . push ( msg . payload ) ; return ; } return app . healthChecker ?. sendJsonRpc ( msg . payload ) ; } const chainSpec = msg . type === "add-chain" ? msg . payload : wellKnownChains . get ( msg . payload ) ?? "" ; return this . #handleSpecMessage ( app , chainSpec , msg . parachainPayload ) ; } ; /**
     * disconnect tells the app to clean up its state and unsubscribe from any
     * active subscriptions and ultimately disconnects the communication port.
     */ disconnect ( app ) { this . #handleDisconnect ( app ) ; } #handleDisconnect = ( app ) : void => { if ( app . state === "disconnected" ) { throw new Error ( "Cannot disconnect - already disconnected" ) ; } app . chain && app . chain . remove ( ) ; app . parachain && app . parachain . remove ( ) ; this . #networks = this . #networks . filter ( ( n ) => n . tabId !== app . tabId ) ; this . unregisterApp ( app ) ; app . state = "disconnected" ; } ; }