Skip to content

Commit

Permalink
feat: introduce provider details in base
Browse files Browse the repository at this point in the history
  • Loading branch information
fapiper committed May 28, 2022
1 parent dee767c commit ad771c5
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 302 deletions.
100 changes: 80 additions & 20 deletions src/core/BaseProvider.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,100 @@
import generateId from '../helpers/generateId'
import LocalStorage from '../helpers/localStorage'
import { parseChainId, validateChainId } from '../helpers/chainId'
import { ConnectResult, Ethereumish, IProvider, ProviderRpcError, ProviderState, ProviderType } from '../types'
import { Connector } from './Connector'
import { ensureChainIdAllowed, parseChainId, validateChainId } from '../helpers/chainId'

export abstract class BaseProvider implements IProvider {
export abstract class AbstractProviderBase<T> implements IProvider {
public id: string
public name: string
public logo: string
public type: ProviderType
private localStorage!: LocalStorage
private options!: ConnectorOptions
public provider!: Ethereumish | null
public options!: T
public address!: string | null
public state!: ProviderState | null
public chainId!: number | null
public error!: Error | null

static CACHED_PROVIDER_KEY = 'CACHED_PROVIDER'
public _initialized: boolean

protected constructor(name: string, logo: string, type: ProviderType) {
private connector!: Connector

protected constructor(name: string, logo: string, type: ProviderType, options: T) {
this.id = generateId(name)
this.name = name
this.logo = logo
this.type = type
this.options = options
this._initialized = false
}

init(options: ConnectorOptions, localStorage: LocalStorage) {
this.options = options
this.localStorage = localStorage
return this
_init(connector: Connector) {
this.connector = connector
this._initialized = true
}

protected abstract _connect(): ConnectResult

async connect(): ConnectResult {
if (!this._initialized) {
throw new Error('Provider not initialized')
}
this.state = ProviderState.LOADING
this.provider = (await this._connect()) as Ethereumish
this.activate()
return this.provider
}

disconnect(): void {
this.deactivate()
}

activate() {
this.address = 'address'
this.chainId = 1
this.error = null
this.connector.provider = this
this.state = this.error ? ProviderState.ERROR : ProviderState.CONNECTED
this.addListener()
}

protected abstract onConnect(options: ConnectorOptions): ConnectResult
deactivate() {
this.address = null
this.state = null
this.chainId = null
this.error = null
this.connector.provider = null
}

addListener() {
this.provider?.on('disconnect', this.onDisconnect)
this.provider?.on('chainChanged', this.onChainChanged)
this.provider?.on('accountsChanged', this.onAccountsChanged)
}

onDisconnect(error: ProviderRpcError | undefined) {
this.deactivate()
this.reportError(error)
}

onChainChanged(chainId: number | string) {
const newChainId = parseChainId(chainId)
this.reportError(validateChainId(newChainId))
const { allowedChainIds } = this.connector.options
if (allowedChainIds) {
this.reportError(ensureChainIdAllowed(newChainId, allowedChainIds))
}
this.chainId = newChainId
}

onAccountsChanged(accounts: string[]) {
this.address = Array.isArray(accounts) ? accounts[0] : ''
}

async connect() {
if (!this.options) {
return null
reportError(error?: Error) {
if (error) {
this.error = error
this.state = ProviderState.ERROR
}
const provider = await this.onConnect(this.options)
const chainId = parseChainId(provider.chainId)
validateChainId(chainId, this.options.chainId || 1)
this.localStorage.set(BaseProvider.CACHED_PROVIDER_KEY, this.id)
return provider
}
}
37 changes: 37 additions & 0 deletions src/core/Connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ConnectorOptions, IProvider } from '../types'
import LocalStorage from '../helpers/localStorage'

export class Connector {
public options: ConnectorOptions
public provider!: IProvider | null
private readonly _providers: IProvider[]

constructor(options: ConnectorOptions, providers: IProvider[]) {
this.options = options
this._providers = providers
}

static async init(options: ConnectorOptions, providers: IProvider[]): Promise<Connector> {
const connector = new Connector(options, providers)
connector._providers.forEach((p) => p._init(connector))
connector.provider = await connector.loadProviderCache(providers)
return connector
}

async loadProviderCache(providers: IProvider[]) {
let cachedProvider = null
const localStorage = new LocalStorage(this.options.cache.key)
const providerId = localStorage.get()
if (providerId) {
const providerById = providers.find((p) => p.id === providerId)
if (providerById) {
cachedProvider = await providerById.connect()
}
}
return cachedProvider
}

listProviders() {
return this._providers
}
}
12 changes: 5 additions & 7 deletions src/core/ExternalProvider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { BaseProvider } from './BaseProvider'
import { AbstractProviderBase } from './BaseProvider'
import { ProviderType } from '../types'

export class ExternalProvider extends BaseProvider {
protected onConnect!: ConnectFn

constructor(name: string, logo: string, type: ProviderType, onConnect: ConnectFn) {
super(name, logo, type)
this.onConnect = onConnect
export abstract class AbstractExternalProvider<T = Record<string, unknown>> extends AbstractProviderBase<T> {
protected constructor(name: string, logo: string, type: ProviderType, options: T = {} as T) {
super(name, logo, type, options)
}
}
13 changes: 7 additions & 6 deletions src/core/InjectedProvider.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { BaseProvider } from './BaseProvider'
import { AbstractProviderBase } from './BaseProvider'
import { ProviderType } from '../types'

export class InjectedProvider extends BaseProvider {
constructor(name: string, logo: string) {
super(name, logo, ProviderType.INJECTED)
export abstract class AbstractInjectedProvider extends AbstractProviderBase<Record<string, unknown>> {
protected constructor(name: string, logo: string) {
super(name, logo, ProviderType.INJECTED, {})
}

protected async onConnect() {
async _connect() {
let provider = null
if (typeof window.ethereum !== 'undefined') {
provider = (window as any).ethereum
provider = window.ethereum
try {
await provider.request({ method: 'eth_requestAccounts' })
} catch (error) {
Expand Down
59 changes: 0 additions & 59 deletions src/core/connector.ts

This file was deleted.

8 changes: 1 addition & 7 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { Connector } from './connector'
import { BaseProvider } from './BaseProvider'
import { ExternalProvider } from './ExternalProvider'
import { InjectedProvider } from './InjectedProvider'
import { Connector } from './Connector'

export default {
Connector,
BaseProvider,
ExternalProvider,
InjectedProvider,
}
14 changes: 10 additions & 4 deletions src/helpers/chainId.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { ChainIdNotAllowedError } from './error'

export const MAX_SAFE_CHAIN_ID = 4503599627370476

export const validateChainId = (chainId: number, allowedChainId: number) => {
export const validateChainId = (chainId: number) => {
if (!Number.isInteger(chainId) || chainId <= 0 || chainId > MAX_SAFE_CHAIN_ID) {
throw new Error(`Invalid chainId ${chainId}`)
return new Error(`Invalid chainId ${chainId}`)
}
if (chainId !== allowedChainId) {
// nothing done
}

export const ensureChainIdAllowed = (chainId: number, allowedChainIds: number[]) => {
const isAllowed = allowedChainIds.some((id) => chainId === id)
if (!isAllowed) {
return new ChainIdNotAllowedError(chainId, allowedChainIds)
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class ProviderNotInitializedError extends Error {
public constructor() {
super(`Provider not initialized`)
this.name = ProviderNotInitializedError.name
Object.setPrototypeOf(this, ProviderNotInitializedError.prototype)
}
}

export class ChainIdNotAllowedError extends Error {
public readonly chainId: number

public constructor(chainId: number, allowedChainIds: number[]) {
super(`chainId ${chainId} not included in ${allowedChainIds.toString()}`)
this.chainId = chainId
this.name = ChainIdNotAllowedError.name
Object.setPrototypeOf(this, ChainIdNotAllowedError.prototype)
}
}
20 changes: 10 additions & 10 deletions src/helpers/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ export default class LocalStorage {
storage?: WindowLocalStorage['localStorage']
enabled = false

constructor() {
constructor(public key: string) {
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
this.storage = window.localStorage
this.enabled = true
}
}

set(key: string, data: any) {
set(data: any) {
const jsonData = JSON.stringify(data)
if (this.enabled) {
this.storage?.setItem(key, jsonData)
this.storage?.setItem(this.key, jsonData)
}
}

get(key: string) {
get() {
let data = null
let raw = null
if (this.enabled) {
raw = this.storage?.getItem(key)
raw = this.storage?.getItem(this.key)
}
if (raw && typeof raw === 'string') {
try {
Expand All @@ -32,18 +32,18 @@ export default class LocalStorage {
return data
}

remove(key: string) {
remove() {
if (this.enabled) {
this.storage?.removeItem(key)
this.storage?.removeItem(this.key)
}
}

update(key: string, data: any) {
const localData = this.get(key) || {}
update(data: any) {
const localData = this.get() || {}
const mergedData = {
...localData,
...data,
}
this.set(key, mergedData)
this.set(mergedData)
}
}
Loading

0 comments on commit ad771c5

Please sign in to comment.