diff --git a/package.json b/package.json index 7fe9002e..7a46eaa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "everscale-inpage-provider", - "version": "0.3.66", + "version": "0.4.0", "description": "Web3-like interface to the Everscale blockchain", "repository": "https://github.com/broxus/everscale-inpage-provider", "main": "dist/index.js", diff --git a/src/adapters.ts b/src/adapters.ts new file mode 100644 index 00000000..b080cc39 --- /dev/null +++ b/src/adapters.ts @@ -0,0 +1,153 @@ +import type { Provider } from './index'; + +declare global { + interface Window { + __ever: Provider | undefined; + __sparx: Provider | undefined; + __hasEverscaleProvider: boolean | undefined; + } +} + +const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; +const ensurePageLoaded = !isBrowser || document.readyState === 'complete' + ? Promise.resolve() + : new Promise(resolve => window.addEventListener('load', () => resolve())); + +/** + * Interface representing a provider adapter. + * This interface defines the methods required to interact with a provider. + * @category Provider + */ +export interface ProviderAdapter { + /** + * Retrieves the provider instance. + */ + getProvider(): Promise | Provider | undefined; + + /** + * Checks if a provider is available. + */ + hasProvider(): Promise | boolean; +} + +/** + * @category Provider + */ +export function hasEverscaleProvider(): Promise { + if (!isBrowser) return Promise.resolve(false); + return ensurePageLoaded.then(() => window.__hasEverscaleProvider === true); +} + +/** + * A static implementation of the `ProviderAdapter` interface that wraps a given provider instance or a promise that resolves to a provider. + * This adapter always indicates the presence of a provider. + * @category Provider + * @implements {ProviderAdapter} + */ +export class StaticProviderAdapter implements ProviderAdapter { + private readonly _provider: Promise | Provider; + + constructor(provider: Promise | Provider) { + this._provider = provider; + } + + public getProvider(): Promise | Provider { + return this._provider; + } + + public hasProvider(): boolean { + return true; + } +} + +/** + * An implementation of the `ProviderAdapter` interface that wraps Ever Wallet provider. + * @category Provider + * @implements {ProviderAdapter} + */ +export class EverscaleProviderAdapter implements ProviderAdapter { + public async getProvider(): Promise { + if (!(await this.hasProvider())) return; + + return new Promise((resolve) => { + if (window.__ever) { + resolve(window.__ever); + } else { + window.addEventListener( + 'ever#initialized', + _ => resolve(window.__ever), + { once: true }, + ); + } + }); + } + + public hasProvider(): Promise { + return hasEverscaleProvider(); + } +} + +/** + * An implementation of the `ProviderAdapter` interface that wraps Sparx provider. + * @category Provider + * @implements {ProviderAdapter} + */ +export class SparxProviderAdapter implements ProviderAdapter { + public async getProvider(): Promise { + if (!(await this.hasProvider())) return; + + return new Promise((resolve) => { + if (window.__sparx) { + resolve(window.__sparx); + } else { + window.addEventListener( + 'sparx#initialized', + _ => resolve(window.__sparx), + { once: true }, + ); + } + }); + } + + public hasProvider(): Promise { + return hasEverscaleProvider(); + } +} + + +/** + * The `FallbackProviderAdapter` class implements the `ProviderAdapter` interface + * and provides a mechanism to use multiple provider adapters in a fallback manner. + * It attempts to use the primary adapter first, and if it fails, it falls back to + * the subsequent adapters in the order they were provided. + * + * @category Provider + * @implements {ProviderAdapter} + */ +export class FallbackProviderAdapter implements ProviderAdapter { + private readonly _adapters: [ProviderAdapter, ...ProviderAdapter[]]; + + constructor(adapter: ProviderAdapter, ...fallbacks: ProviderAdapter[]) { + this._adapters = [adapter, ...fallbacks]; + } + + public async getProvider(): Promise { + for (const adapter of this._adapters) { + const provider = await adapter.getProvider(); + if (provider) return provider; + } + + return undefined; + } + + public async hasProvider(): Promise { + for (const adapter of this._adapters) { + if (await adapter.hasProvider()) return true; + } + return false; + } +} + +export function isProviderAdapter(value: any): value is ProviderAdapter { + return !!value && 'getProvider' in value && 'hasProvider' in value; +} diff --git a/src/index.ts b/src/index.ts index 97a0e6da..decc4a2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,9 @@ import { import { Address, DelayedTransactions, getUniqueId } from './utils'; import * as subscriber from './stream'; import * as contract from './contract'; +import { EverscaleProviderAdapter, isProviderAdapter, ProviderAdapter, StaticProviderAdapter } from './adapters'; +export * from './adapters'; export * from './api'; export * from './models'; export * from './contract'; @@ -70,58 +72,24 @@ export interface Provider { */ export type ProviderProperties = { /*** - * Ignore injected provider and try to use `fallback` instead. - * @default false + * Provider or provider adapter to use + * @default EverscaleProviderAdapter */ - forceUseFallback?: boolean; - /*** - * Provider factory which will be called if injected provider was not found. - * Can be used for initialization of the standalone Everscale client - */ - fallback?: () => Promise; + provider?: Promise | Provider | ProviderAdapter; }; -declare global { - interface Window { - __ever: Provider | undefined; - __hasEverscaleProvider: boolean | undefined; - ton: Provider | undefined; - hasTonProvider: boolean | undefined; - } -} - -const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; - -let ensurePageLoaded: Promise; -if (!isBrowser || document.readyState === 'complete') { - ensurePageLoaded = Promise.resolve(); -} else { - ensurePageLoaded = new Promise(resolve => { - window.addEventListener('load', () => { - resolve(); - }); - }); -} - -const getProvider = (): Provider | undefined => (isBrowser ? window.__ever || window.ton : undefined); - -/** - * @category Provider - */ -export async function hasEverscaleProvider(): Promise { - if (!isBrowser) { - return false; - } - - await ensurePageLoaded; - return window.__hasEverscaleProvider === true || window.hasTonProvider === true; +function getAdapter(properties: ProviderProperties): ProviderAdapter { + if (!properties.provider) return new EverscaleProviderAdapter(); + return isProviderAdapter(properties.provider) + ? properties.provider + : new StaticProviderAdapter(properties.provider); } /** * @category Provider */ export class ProviderRpcClient { - private readonly _properties: ProviderProperties; + private readonly _adapter: ProviderAdapter; private readonly _api: RawProviderApiMethods; private readonly _initializationPromise: Promise; private readonly _subscriptions: { [K in ProviderEvent]: Map) => void> } = { @@ -141,7 +109,7 @@ export class ProviderRpcClient { public Subscriber: new () => subscriber.Subscriber; constructor(properties: ProviderProperties = {}) { - this._properties = properties; + this._adapter = getAdapter(properties); const self = this; @@ -169,64 +137,24 @@ export class ProviderRpcClient { { get: (_object: ProviderRpcClient, method: K) => - (params: RawProviderApiRequestParams) => { - if (this._provider != null) { - return this._provider.request({ method, params }); - } else { - throw new ProviderNotInitializedException(); - } - }, + (params: RawProviderApiRequestParams) => { + if (this._provider != null) { + return this._provider.request({ method, params }); + } else { + throw new ProviderNotInitializedException(); + } + }, }, ) as unknown as RawProviderApiMethods; - if (properties.forceUseFallback === true) { - this._initializationPromise = - properties.fallback != null - ? properties.fallback().then(provider => { - this._provider = provider; - }) - : Promise.resolve(); - } else { - // Initialize provider with injected object by default - this._provider = getProvider(); - if (this._provider != null) { - // Provider as already injected - this._initializationPromise = Promise.resolve(); - } else { - // Wait until page is loaded and initialization complete - this._initializationPromise = hasEverscaleProvider() - .then( - hasProvider => - new Promise(resolve => { - if (!hasProvider) { - // Fully loaded page doesn't even contain provider flag - return resolve(); - } - - // Wait injected provider initialization otherwise - this._provider = getProvider(); - if (this._provider != null) { - resolve(); - } else { - const eventName = window.__hasEverscaleProvider === true ? 'ever#initialized' : 'ton#initialized'; - window.addEventListener(eventName, _ => { - this._provider = getProvider(); - resolve(); - }); - } - }), - ) - .then(async () => { - if (this._provider == null && properties.fallback != null) { - this._provider = await properties.fallback(); - } - }); - } - } + const providerPromise = Promise.resolve(this._adapter.getProvider()); + this._initializationPromise = providerPromise.then(provider => { + this._provider = provider; + }); // Will only register handlers for successfully loaded injected provider this._initializationPromise.then(() => { - if (this._provider != null) { + if (this._provider) { this._registerEventHandlers(this._provider); } }); @@ -237,10 +165,7 @@ export class ProviderRpcClient { * there is a fallback provider. */ public async hasProvider(): Promise { - if (this._properties.fallback != null) { - return true; - } - return hasEverscaleProvider(); + return this._adapter.hasProvider(); } /** @@ -405,7 +330,7 @@ export class ProviderRpcClient { constructor( private readonly _subscribe: (s: SubscriptionImpl) => Promise, private readonly _unsubscribe: () => Promise, - ) {} + ) { } on(eventName: 'data', listener: (data: ProviderEventData) => void): this; on(eventName: 'subscribed', listener: () => void): this; @@ -973,10 +898,10 @@ export class ProviderRpcClient { bounce: args.bounce, payload: args.payload ? { - abi: args.payload.abi, - method: args.payload.method, - params: serializeTokensObject(args.payload.params), - } + abi: args.payload.abi, + method: args.payload.method, + params: serializeTokensObject(args.payload.params), + } : undefined, stateInit: args.stateInit, }); @@ -1017,10 +942,10 @@ export class ProviderRpcClient { bounce: args.bounce, payload: args.payload ? { - abi: args.payload.abi, - method: args.payload.method, - params: serializeTokensObject(args.payload.params), - } + abi: args.payload.abi, + method: args.payload.method, + params: serializeTokensObject(args.payload.params), + } : undefined, stateInit: args.stateInit, }) @@ -1167,8 +1092,8 @@ export class ProviderNotInitializedException extends Error { */ export type RawRpcMethod

= RawProviderApiRequestParams

extends undefined - ? () => Promise> - : (args: RawProviderApiRequestParams

) => Promise>; + ? () => Promise> + : (args: RawProviderApiRequestParams

) => Promise>; /** * @category Provider @@ -1182,23 +1107,23 @@ export type RawProviderApiMethods = { */ export type GetExpectedAddressParams = Abi extends { data: infer D } ? { - /** - * Base64 encoded TVC file - */ - tvc: string; - /** - * Contract workchain. 0 by default - */ - workchain?: number; - /** - * Public key, which will be injected into the contract. 0 by default - */ - publicKey?: string; - /** - * State init params - */ - initParams: MergeInputObjectsArray; - } + /** + * Base64 encoded TVC file + */ + tvc: string; + /** + * Contract workchain. 0 by default + */ + workchain?: number; + /** + * Public key, which will be injected into the contract. 0 by default + */ + publicKey?: string; + /** + * State init params + */ + initParams: MergeInputObjectsArray; + } : never; /** @@ -1213,21 +1138,21 @@ export type SetCodeSaltParams

= { * Base64 encoded salt (as BOC) or params of boc encoder */ salt: - | string - | { - /** - * ABI version. 2.2 if not specified otherwise - */ - abiVersion?: AbiVersion; - /** - * Cell structure - */ - structure: P; - /** - * Cell data - */ - data: MergeInputObjectsArray

; - }; + | string + | { + /** + * ABI version. 2.2 if not specified otherwise + */ + abiVersion?: AbiVersion; + /** + * Cell structure + */ + structure: P; + /** + * Cell data + */ + data: MergeInputObjectsArray

; + }; }; /**