import { Dictionary } from 'infinitymint/dist/app/helpers'; import { ProjectCache } from 'infinitymint/dist/app/projects'; import { InfinityMintClientConfig } from './core/interfaces'; import { fetchProject, hasSavedProject, Project } from './core/project'; import { apiGet, chainIds, getApiEndpoint, isApiAlive, log, toProjectFullName, warning, } from './utils/helpers'; import { Web3Provider, Provider, JsonRpcProvider, } from '@ethersproject/providers'; import { ethers, Signer } from 'ethers'; import storageController from './core/storage'; import WebEvents from './webEvents'; import { requestAccounts, requestBalance, requestChainId } from './utils/web3'; export class InfinityMintClient { public projects: Dictionary = {}; public apiAccess = false; public web3Access = false; public loaded = false; public networkAccess = false; public signers: Dictionary = {}; public addresses: string[]; public providers: Dictionary< Web3Provider | Provider | ethers.providers.Web3Provider > = {}; public staticProviders: Dictionary = {}; public defaultNetwork: string; public currentNetwork: { chainId: number; name: string; url?: string; }; public defaultProject: Project; public defaultChainId: number; public projectsCache: ProjectCache; public config: InfinityMintClientConfig; private walletError: Error[]; constructor(config: InfinityMintClientConfig) { this.defaultNetwork = config?.network?.default?.developer || config?.network?.default?.production || 'ganache'; this.config = config; this.registerKeybindings(); } public getCurrentAddress(): string { return this.addresses?.[0] || '0x00000000000000000000000000000000'; } async reset() { this.destroy(); await this.load(); } private async setWalletError(reason: string) { if (!this.walletError) this.walletError = []; this.walletError.push(new Error(reason)); return false; } async wallet() { this.web3Access = false; this.networkAccess = false; if (!(window as any).ethereum) return this.setWalletError( 'Wallet has not been installed on this browser' ); //our own custom implementation here if (!this.config?.network?.callbacks?.connect) { try { //check it with the balance request let accounts = await requestAccounts(); if (accounts.length === 0) return this.setWalletError( 'You have not given us any accounts to work with!' ); //now lets check what we've actually been given let provider = new ethers.providers.Web3Provider( (window as any).ethereum ); //we have network access this.networkAccess = true; let chainId = await requestChainId(provider); let network = Object.keys(chainIds) .filter((network) => chainIds[network] === chainId) .pop(); let signer = await provider.getSigner(); let account = await signer.getAddress(); await requestBalance(provider, account); this.setProviderForNetwork(network, provider); this.addresses = accounts; this.signers[account] = signer; this.web3Access = true; } catch (error) { return this.setWalletError(error.message); } } else { try { await this.config?.network?.callbacks?.connect(); } catch (error) { return this.setWalletError(error.message); } } return true; } public getCurrentProvider(): Web3Provider { return this.providers[this.defaultNetwork] as Web3Provider; } public getCurrentStaticProvider(): JsonRpcProvider { return this.staticProviders[this.defaultNetwork]; } public addLocalRequiredProject(projectFullName: string) { let project = this.projects[projectFullName]; if (!project) return; storageController.setGlobalPreference('requiredProjects', { ...storageController.getGlobalPreference('requiredProjects'), [projectFullName]: { name: project.name, version: project.version, network: project.deployedProject.network.name, }, }); storageController.save(); } /** * */ public destroy() { //disconnect providers Object.keys(this.providers).forEach((network) => { let provider = this.providers[network]; if (provider) provider.removeAllListeners(); }); Object.keys(this.staticProviders).forEach((network) => { let provider = this.staticProviders[network]; if (provider) provider.removeAllListeners(); }); this.providers = {}; this.staticProviders = {}; this.signers = {}; this.addresses = []; this.web3Access = false; this.networkAccess = false; this.loaded = false; this.projects = {}; delete this.projectsCache; } public getOldestProject( projectName: string, projectNetwork: string, startingVersion: string = '0.0.0' ) { return this.getLatestProject( projectName, projectNetwork, startingVersion, true ); } public getLatestProject( projectName: string, projectNetwork: string, startingVersion: string = '0.0.0', getOldest = false ) { let selection = Object.keys(this.projects).filter((projectFullName) => { let project = this.projects[projectFullName]; return ( project.name === projectName && project.deployedProject.network.name === projectNetwork && parseFloat(project.version.version) > parseFloat(startingVersion) ); }); if (selection.length === 0) return null; //filter selection so the latest version is first selection.sort((a, b) => { let projectA = this.projects[a]; let projectB = this.projects[b]; return ( parseFloat(projectB.version.version) - parseFloat(projectA.version.version) ); }); if (getOldest) return this.projects[selection[selection.length - 1]]; return this.projects[selection[0]]; } public removeLocalRequiredProject(projectFullName: string) { let project = this.projects[projectFullName]; if (!project) return; let requiredProjects = storageController.getGlobalPreference('requiredProjects'); delete requiredProjects[projectFullName]; storageController.setGlobalPreference( 'requiredProjects', requiredProjects ); storageController.save(); } async load(abortController?: AbortController) { this.apiAccess = await isApiAlive(); //do wallet if (!this.web3Access || this.networkAccess) await this.wallet(); let requireProjects = this.config.projects?.require || []; requireProjects = { ...requireProjects, ...(storageController.getGlobalPreference('requiredProjects') || {}), }; for (let projectKey of Object.keys(requireProjects)) { let project = requireProjects[projectKey]; if (project.network && project.network instanceof Array === false) { project.network = [project.network]; } else project.network = [this.defaultNetwork]; for (let network of project.network) { if (!project.version) project.version = '1.0.0'; if ( storageController.getGlobalPreference( toProjectFullName( project.name || projectKey, project.version, network ) ) ) { console.log( 'Using local project => ' + (project.name || projectKey) ); this.projects[ toProjectFullName( project.name || projectKey, project.version, network ) ] = Project.load( project.name || projectKey, project.version, this, network ); log( `Loaded Project => ${project.name || projectKey}@${ project.version }_${network}` ); } else try { let fetchedProject = await fetchProject( project.name || projectKey, project.version || '1.0.0', network || this.defaultNetwork, this, abortController, !this.apiAccess ); log( 'Fetched Project => ' + projectKey + '@' + project.version + '_' + (network || this.defaultNetwork) ); let key = fetchedProject.deployedProject?.name + '@' + fetchedProject.deployedProject?.version?.version + '_' + fetchedProject.deployedProject?.network?.name; this.projects[key] = fetchedProject; log( `Loaded Project => ${project.name || projectKey}@${ project.version || '1.0.0' }_${network || this.defaultNetwork}` ); } catch (error) { log( 'Bad Project => ' + projectKey + '@' + project.version + '_' + (network || this.defaultNetwork) ); console.error(error); } } } if (!this.apiAccess) { warning('API is not available'); } else { //fetch API related things this.projectsCache = await this.getProjectsCacheFromApi(); if (this.config.network?.setDefaultUsingApi) { let result = await this.getNetworkFromApi(); log('Setting default network using API', { name: result.name, chainId: result.chainId, }); this.defaultNetwork = result.name; this.defaultChainId = result.chainId; } } if ( this.config.network?.default && (!this.apiAccess || !this.config.network?.setDefaultUsingApi) ) { this.defaultNetwork = this.config.network.default.developer; if (!chainIds[this.config.network.default.developer]) throw new Error(` Invalid network name has no chain Id: ${this.config.network.default.developer} `); this.defaultChainId = chainIds[this.config.network.default.developer]; } let defaultProject = this.config?.projects?.default?.dev; if (this.config?.projects?.forceProduction) defaultProject = this.config?.projects?.default?.production; if (!defaultProject) { warning( 'No default project set, assuming first project (which is ' + Object.keys(this.projects)[0] + ')' ); } else log('Default project set to ' + defaultProject); //just load the first required project if no default is found defaultProject = defaultProject || Object.keys(this.projects)[0]; if ( !this.config.projects?.ignoreCustomDefaultProject && storageController.getGlobalPreference('defaultProject') && this.projects[ storageController.getGlobalPreference('defaultProject') ] ) { console.warn('Using local default project => ' + defaultProject); defaultProject = storageController.getGlobalPreference('defaultProject'); } if ( !this.web3Access || !this.networkAccess || this.config.network?.alwaysCreateStaticProviders ) this.createStaticProviders(); if (defaultProject) { if (this.config.projects.require[defaultProject]) defaultProject = defaultProject + '@' + this.config.projects.require[defaultProject].version + '_' + (!this.config.projects.require[defaultProject].network ? this.defaultNetwork : this.config.projects.require[defaultProject] .network instanceof Array ? this.config.projects.require[defaultProject] .network[0] : this.config.projects.require[defaultProject].network); await this.loadProject(defaultProject); this.defaultProject = this.projects[defaultProject]; //will check chain for new project if (this.networkAccess) { log('Searching for updates from chain'); let updates = await this.defaultProject.checkForUpdates(); if (updates.newVersion) { let newProject = Project.create( await this.defaultProject.fetchProjectFromWeb3(), this ); this.projects[newProject.projectFullName] = newProject; await this.loadProject(newProject.projectFullName); this.defaultProject = newProject; this.removeLocalRequiredProject( toProjectFullName( newProject.name, updates.currentVersion, newProject.deployedProject.network.name ) ); this.addLocalRequiredProject(newProject.projectFullName); this.saveDefaultProject(newProject); WebEvents.emit( 'updatedProject', newProject, this.projects[ toProjectFullName( newProject.name, updates.currentVersion, newProject.deployedProject.network.name ) ] ); } } } this.loaded = true; } public registerKeybindings() { const toggleDashboard = (event) => { if (event.ctrlKey && event.key === 'q') { WebEvents.emit('toggleDashboard'); } }; document.removeEventListener('keydown', toggleDashboard); document.addEventListener('keydown', toggleDashboard); } /** * Will set the default project to load * @param project */ public saveDefaultProject(project: Project) { storageController.setGlobalPreference( 'defaultProject', project.projectFullName ); storageController.save(); } public getDefaultProject() { return this.defaultProject; } public async loadProject( projectFullName: string, project?: Project, instantiateContracts: boolean = true ) { project = project || this.projects[projectFullName]; if (!project) throw new Error('Project not found: ' + projectFullName); await project.initializeResources(); let setNetworkProvider = this.web3Access && this.networkAccess; log('has network provider => ' + (setNetworkProvider ? 'yes' : 'no')); this.setProjectStaticProvider( project.deployedProject.name, project.deployedProject.version.version, project.deployedProject.network.name ); if (setNetworkProvider) this.setProjectProvider( project.deployedProject.name, project.deployedProject.version.version, project.deployedProject.network.name ); if (instantiateContracts) await this.instantiateContracts( project.deployedProject.name, project.deployedProject.version.version, project.deployedProject.network.name ); } public async instantiateContracts( projectName: string, version: string, network: string ) { let project = this.getProject(projectName, network, version); if (!project) throw new Error( 'Project not found: ' + toProjectFullName(projectName, version, network) ); let contracts = {}; Object.values(project.deployedProject.deployments).forEach( async (contract) => { if ( contracts[ contract.name || contract.contractName || contract.key ] ) return; project.getContract( contract.name || contract.contractName || contract.key, null, false, true ); contracts[ contract.name || contract.contractName || contract.key ] = true; } ); } public setProjectProvider( projectName: string, version: string, network: string ) { let project = this.getProject(projectName, network, version); if (!project) throw new Error( 'Project not found: ' + toProjectFullName(projectName, version, network) ); project.setProvider(this.providers[network]); } public createStaticProviders() { let providers = { ganache: 'http://localhost:8545', ...(this.config.network?.providers || {}), }; log('Creating static providers'); Object.keys(providers).forEach((network) => { if (this.staticProviders[network]) delete this.staticProviders[network]; this.staticProviders[network] = new JsonRpcProvider( providers[network] ); log('Added static provider => ' + network); }); } public setProjectStaticProvider( projectName: string, version: string, network: string ) { let project = this.getProject(projectName, network, version); if (!project) throw new Error('Project not found'); if (!this.staticProviders[network]) throw new Error('No static provider found for ' + network); project.setStaticProvider(this.staticProviders[network]); } public getProject(name: string, network: string, version?: string) { version = version || '1.0.0'; let result = this.projects[toProjectFullName(name, version, network)]; return result; } public async loadProjectFromStorage( name: string, version: string = '1.0.0', network?: string, instantiate: boolean = true, requireNetworkChange?: boolean ) { if (!hasSavedProject(name, version, network)) throw new Error( 'Saved project not found: ' + toProjectFullName(name, version, network) ); let result = Project.load(name, version, this, network); if (instantiate) await this.loadProject(null, result); return result; } public async createUpdate(projectName: string, newVersion?: string) { let project = this.getProject(projectName, this.defaultNetwork); if (!project) throw new Error('Project not found'); newVersion = newVersion || project.deployedProject.version.version .split('.') .map((v, i) => (i === 2 ? parseInt(v) + 1 : v)) .join('.'); if (this.hasProject(projectName, this.defaultNetwork, newVersion)) throw new Error('Project already exists'); let result = project.clone(newVersion); this.projects[ toProjectFullName(projectName, newVersion, this.defaultNetwork) ] = result; return await this.loadProject( toProjectFullName(projectName, newVersion, this.defaultNetwork) ); } public getContract( projectName: string, contractName: string, network?: string, version?: string ) { network = network || this.defaultNetwork; let project = this.getProject(projectName, network, version); if (!project) throw new Error( 'Project not found: ' + toProjectFullName(projectName, version, network) ); return project.getContract(contractName, undefined, true); } public hasProject(name: string, network?: string, version?: string) { network = network || this.defaultNetwork; version = version || '1.0.0'; return !!this.projects[toProjectFullName(name, version, network)]; } public getSignedContract( projectName: string, contractName: string, network?: string, version?: string ) { network = network || this.defaultNetwork; let project = this.getProject(projectName, network, version); if (!project) throw new Error( 'Project not found: ' + toProjectFullName(projectName, version, network) ); return project.getContract(contractName, undefined, true); } public setStaticProviderForNetwork( network: string, provider: JsonRpcProvider ) { this.staticProviders[network] = provider; } public getProviderForNetwork(network: string) { return this.providers[network]; } public getStaticProviderForNetwork(network: string) { return this.staticProviders[network]; } public setProviderForNetwork( network: string, provider: Web3Provider | Provider | ethers.providers.Web3Provider ) { if (network === 'mainnet') network = 'ethereum'; log('Setting provider for network ' + network); this.providers[network] = provider; } public async getProjectFromPublic( projectName: string, version: string, network: string, instantiate: boolean = true, abortController?: AbortController ) { let project = await fetchProject( projectName, version, network, this, abortController, true ); if (!instantiate) return project; this.projects[toProjectFullName(projectName, version, network)] = project; if (this.web3Access && this.networkAccess) { this.setProjectProvider( projectName, version, project.deployedProject.network.name ); } else { this.setProjectStaticProvider( projectName, version, project.deployedProject.network.name ); } return project; } public async getProjectFromApi( projectName: string, version: string, network: string, type: 'deployed' | 'compiled' = 'deployed', instantiate: boolean = true, forceDeveloper: boolean = false, abortController?: AbortController ): Promise { if (!this.apiAccess) throw new Error('API is not available'); let json = await apiGet( '/projects/get', { name: projectName, version: version, network: network, type: type, }, forceDeveloper, abortController ); let project = Project.create(json, this); if (!instantiate) return project; this.projects[toProjectFullName(projectName, version, network)] = project; await this.loadProject( toProjectFullName(projectName, version, network) ); return project; } public async getConfigFromApi(abortController?: AbortController) { if (!this.apiAccess) throw new Error('API is not available'); let endPoint = getApiEndpoint('/meta/config'); let response = await fetch(endPoint, { signal: abortController?.signal, }); let json = await response.json(); return json; } async getNetworkFromApi(): Promise<{ name: string; chainId: number }> { if (!this.apiAccess) throw new Error('API is not available'); let endPoint = getApiEndpoint('/meta/network'); let response = await fetch(endPoint); let json = await response.json(); return json; } async getProjectsCacheFromApi( abortController?: AbortController ): Promise { if (!this.apiAccess) throw new Error('API is not available'); let endPoint = getApiEndpoint('/meta/projects/all'); let response = await fetch(endPoint, { signal: abortController?.signal, }); let json = await response.json(); return json; } }