diff --git a/.changeset/tasty-falcons-sing.md b/.changeset/tasty-falcons-sing.md new file mode 100644 index 00000000000..dc1a805c902 --- /dev/null +++ b/.changeset/tasty-falcons-sing.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/account": patch +--- + +implement wallet connectors diff --git a/.eslintrc.js b/.eslintrc.js index 7bc0e64fb89..1904ff8fbf6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,8 @@ module.exports = { ], '@typescript-eslint/no-non-null-assertion': 1, // Disable error on devDependencies importing since this isn't a TS library + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'no-await-in-loop': 0, 'prefer-destructuring': 0, @@ -36,7 +38,6 @@ module.exports = { 'no-underscore-dangle': 'off', 'class-methods-use-this': 'off', 'no-plusplus': 'off', - 'no-param-reassign': ['error', { props: false }], '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/lines-between-class-members': [ 'error', diff --git a/package.json b/package.json index 5b7030239b6..d7bbd18fbf9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test:watch": "vitest --watch --config vite.node.config.mts $(scripts/tests-find.sh --node)", "test:validate": "./scripts/tests-validate.sh", "test:browser": "vitest --run --coverage --config vite.browser.config.mts $(scripts/tests-find.sh --browser)", + "test:browser:filter": "vitest --run --coverage --config vite.browser.config.mts", "test:e2e": "vitest --run --config vite.node.config.mts $(scripts/tests-find.sh --e2e)", "lint": "run-s lint:check prettier:check", "lint:check": "eslint . --ext .ts --max-warnings 0", diff --git a/packages/abi-coder/src/coders/v0/struct.ts b/packages/abi-coder/src/coders/v0/struct.ts index d04203c5f25..15ea9ae46fe 100644 --- a/packages/abi-coder/src/coders/v0/struct.ts +++ b/packages/abi-coder/src/coders/v0/struct.ts @@ -74,6 +74,7 @@ export class StructCoder> extends Coder< newOffset += getWordSizePadding(newOffset); } + // eslint-disable-next-line no-param-reassign obj[fieldName as keyof DecodedValueOf] = decoded; return obj; }, {} as DecodedValueOf); diff --git a/packages/abi-coder/src/coders/v1/struct.ts b/packages/abi-coder/src/coders/v1/struct.ts index 632649ba14b..7a0aa65dd52 100644 --- a/packages/abi-coder/src/coders/v1/struct.ts +++ b/packages/abi-coder/src/coders/v1/struct.ts @@ -42,6 +42,7 @@ export class StructCoder> extends Coder< let decoded; [decoded, newOffset] = fieldCoder.decode(data, newOffset); + // eslint-disable-next-line no-param-reassign obj[fieldName as keyof DecodedValueOf] = decoded; return obj; }, {} as DecodedValueOf); diff --git a/packages/account/package.json b/packages/account/package.json index 7118fefd85d..abdc52ef4af 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -56,10 +56,11 @@ "@fuel-ts/hasher": "workspace:*", "@fuel-ts/interfaces": "workspace:*", "@fuel-ts/math": "workspace:*", + "@fuel-ts/merkle": "workspace:*", "@fuel-ts/transactions": "workspace:*", "@fuel-ts/utils": "workspace:*", - "@fuel-ts/merkle": "workspace:*", "@fuel-ts/versions": "workspace:*", + "@fuels/assets": "^0.1.4", "@fuels/vm-asm": "0.42.1", "graphql": "^16.6.0", "graphql-request": "5.0.0", @@ -68,7 +69,9 @@ "tai64": "^1.0.0", "events": "^3.3.0", "@noble/curves": "^1.3.0", + "dexie-observable": "4.0.1-beta.13", "ethers": "^6.7.1", + "json-rpc-2.0": "^1.7.0", "portfinder": "^1.0.32", "tree-kill": "^1.2.2", "uuid": "^9.0.0" diff --git a/packages/account/src/account.ts b/packages/account/src/account.ts index 435ea5610dd..57912a7ba31 100644 --- a/packages/account/src/account.ts +++ b/packages/account/src/account.ts @@ -8,6 +8,7 @@ import { bn } from '@fuel-ts/math'; import { getBytesCopy } from 'ethers'; import type { BytesLike } from 'ethers'; +import type { FuelConnector } from './connectors'; import type { TransactionRequestLike, CallResult, @@ -18,10 +19,10 @@ import type { Message, Resource, ExcludeResourcesOption, - TransactionResponse, Provider, ScriptTransactionRequestLike, ProviderSendTxParams, + TransactionResponse, } from './providers'; import { withdrawScript, @@ -50,15 +51,18 @@ export class Account extends AbstractAccount { */ protected _provider?: Provider; + protected _connector?: FuelConnector; + /** * Creates a new Account instance. * * @param address - The address of the account. * @param provider - A Provider instance (optional). */ - constructor(address: string | AbstractAddress, provider?: Provider) { + constructor(address: string | AbstractAddress, provider?: Provider, connector?: FuelConnector) { super(); this._provider = provider; + this._connector = connector; this.address = Address.fromDynamicInput(address); } @@ -478,6 +482,13 @@ export class Account extends AbstractAccount { return this.sendTransaction(request); } + async signMessage(message: string): Promise { + if (!this._connector) { + throw new FuelError(ErrorCode.MISSING_CONNECTOR, 'A connector is required to sign messages.'); + } + return this._connector.signMessage(this.address.toString(), message); + } + /** * Sends a transaction to the network. * @@ -488,6 +499,11 @@ export class Account extends AbstractAccount { transactionRequestLike: TransactionRequestLike, options?: Pick ): Promise { + if (this._connector) { + return this.provider.getTransactionResponse( + await this._connector.sendTransaction(this.address.toString(), transactionRequestLike) + ); + } const transactionRequest = transactionRequestify(transactionRequestLike); await this.provider.estimateTxDependencies(transactionRequest); return this.provider.sendTransaction(transactionRequest, { diff --git a/packages/account/src/connectors/fuel-connector.ts b/packages/account/src/connectors/fuel-connector.ts new file mode 100644 index 00000000000..ec43bd6f0e6 --- /dev/null +++ b/packages/account/src/connectors/fuel-connector.ts @@ -0,0 +1,261 @@ +/* eslint-disable @typescript-eslint/require-await */ +import { EventEmitter } from 'events'; + +import type { TransactionRequestLike } from '../providers'; + +import { FuelConnectorEventTypes } from './types'; +import type { + FuelConnectorEvents, + ConnectorMetadata, + FuelABI, + Network, + FuelEventArg, + Version, + Asset, +} from './types'; + +/** + * @name FuelConnector + * + * Wallet Connector is a interface that represents a Wallet Connector and all the methods + * that should be implemented to be compatible with the Fuel SDK. + */ +export abstract class FuelConnector extends EventEmitter { + name: string = ''; + metadata: ConnectorMetadata = {} as ConnectorMetadata; + connected: boolean = false; + installed: boolean = false; + events = FuelConnectorEventTypes; + + /** + * Should return true if the connector is loaded + * in less then one second. + * + * @returns Always true. + */ + async ping(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return the current version of the connector + * and the network version that is compatible. + * + * @returns boolean - connection status. + */ + async version(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return true if the connector is connected + * to any of the accounts available. + * + * @returns The connection status. + */ + async isConnected(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return all the accounts authorized for the + * current connection. + * + * @returns The accounts addresses strings + */ + async accounts(): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Should start the connection process and return + * true if the account authorize the connection. + * + * and return false if the user reject the connection. + * + * @emits accounts + * @returns boolean - connection status. + */ + async connect(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should disconnect the current connection and + * return false if the disconnection was successful. + * + * @emits assets connection + * @returns The connection status. + */ + async disconnect(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should start the sign message process and return + * the signed message. + * + * @param address - The address to sign the message + * @param message - The message to sign all text will be treated as text utf-8 + * + * @returns Message signature + */ + async signMessage(_address: string, _message: string): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should start the send transaction process and return + * the transaction id submitted to the network. + * + * If the network is not available for the connection + * it should throw an error to avoid the transaction + * to be sent to the wrong network and lost. + * + * @param address - The address to sign the transaction + * @param transaction - The transaction to send + * + * @returns The transaction id + */ + async sendTransaction(_address: string, _transaction: TransactionRequestLike): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return the current account selected inside the connector, if the account + * is authorized for the connection. + * + * If the account is not authorized it should return null. + * + * @returns The current account selected otherwise null. + */ + async currentAccount(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should add the the assets metadata to the connector and return true if the asset + * was added successfully. + * + * If the asset already exists it should throw an error. + * + * @emits assets + * @param assets - The assets to add the metadata to the connection. + * @throws Error if the asset already exists + * @returns True if the asset was added successfully + */ + async addAssets(_assets: Array): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should add the the asset metadata to the connector and return true if the asset + * was added successfully. + * + * If the asset already exists it should throw an error. + * + * @emits assets + * @param asset - The asset to add the metadata to the connection. + * @throws Error if the asset already exists + * @returns True if the asset was added successfully + */ + async addAsset(_asset: Asset): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return all the assets added to the connector. If a connection is already established. + * + * @returns Array of assets metadata from the connector vinculated to the all accounts from a specific Wallet. + */ + async assets(): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Should start the add network process and return true if the network was added successfully. + * + * @emits networks + * @throws Error if the network already exists + * @param networkUrl - The URL of the network to be added. + * @returns Return true if the network was added successfully + */ + async addNetwork(_networkUrl: string): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should start the select network process and return true if the network has change successfully. + * + * @emits networks + * @throws Error if the network already exists + * @param network - The network to be selected. + * @returns Return true if the network was added successfully + */ + async selectNetwork(_network: Network): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return all the networks available from the connector. If the connection is already established. + * + * @returns Return all the networks added to the connector. + */ + async networks(): Promise> { + throw new Error('Method not implemented.'); + } + + /** + * Should return the current network selected inside the connector. Even if the connection is not established. + * + * @returns Return the current network selected inside the connector. + */ + async currentNetwork(): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should add the ABI to the connector and return true if the ABI was added successfully. + * + * @param contractId - The contract id to add the ABI. + * @param abi - The JSON ABI that represents a contract. + * @returns Return true if the ABI was added successfully. + */ + async addABI(_contractId: string, _abi: FuelABI): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return the ABI from the connector vinculated to the all accounts from a specific Wallet. + * + * @param id - The contract id to get the ABI. + * @returns The ABI if it exists, otherwise return null. + */ + async getABI(_id: string): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Should return true if the abi exists in the connector vinculated to the all accounts from a specific Wallet. + * + * @param id - The contract id to get the abi + * @returns Returns true if the abi exists or false if not. + */ + async hasABI(_id: string): Promise { + throw new Error('Method not implemented.'); + } + + /** + * Event listener for the connector. + * + * @param eventName - The event name to listen + * @param listener - The listener function + */ + on>( + eventName: E, + listener: (data: D) => void + ): this { + super.on(eventName, listener); + return this; + } +} diff --git a/packages/account/src/connectors/fuel.ts b/packages/account/src/connectors/fuel.ts new file mode 100644 index 00000000000..fa1c46300b0 --- /dev/null +++ b/packages/account/src/connectors/fuel.ts @@ -0,0 +1,453 @@ +import type { AbstractAddress } from '@fuel-ts/interfaces'; + +import { Account } from '../account'; +import { Provider } from '../providers'; +import type { StorageAbstract } from '../wallet-manager'; + +import { FuelConnector } from './fuel-connector'; +import { + FuelConnectorMethods, + FuelConnectorEventTypes, + FuelConnectorEventType, + LocalStorage, +} from './types'; +import type { Network, FuelConnectorEventsType, TargetObject } from './types'; +import type { CacheFor } from './utils'; +import { cacheFor, deferPromise, withTimeout } from './utils'; + +// This is the time to wait for the connector +// to be available before returning false for hasConnector. +const HAS_CONNECTOR_TIMEOUT = 2_000; +// The time to cache the ping result, as is not +// expected to change the availability of the connector to +// change too often we can safely cache the result for 5 seconds +// at minimum. +const PING_CACHE_TIME = 5_000; + +export type FuelConfig = { + connectors?: Array; + storage?: StorageAbstract | null; + targetObject?: TargetObject; + devMode?: boolean; +}; + +export type FuelConnectorSelectOptions = { + emitEvents?: boolean; +}; + +export type Status = { + installed: boolean; + connected: boolean; +}; + +export class Fuel extends FuelConnector { + static STORAGE_KEY = 'fuel-current-connector'; + static defaultConfig: FuelConfig = {}; + private _storage?: StorageAbstract | null = null; + private _connectors: Array = []; + private _targetObject: TargetObject | null = null; + private _unsubscribes: Array<() => void> = []; + private _targetUnsubscribe: () => void; + private _pingCache: CacheFor = {}; + private _currentConnector?: FuelConnector | null; + + constructor(config: FuelConfig = Fuel.defaultConfig) { + super(); + // Increase the limit of listeners + this.setMaxListeners(1_000); + // Set all connectors + this._connectors = config.connectors ?? []; + // Set the target object to listen for global events + this._targetObject = this.getTargetObject(config.targetObject); + // Set default storage + this._storage = config.storage === undefined ? this.getStorage() : config.storage; + // Setup all methods + this.setupMethods(); + // Get the current connector from the storage + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.setDefaultConnector(); + // Setup new connector listener for global events + this._targetUnsubscribe = this.setupConnectorListener(); + } + + /** + * Return the target object to listen for global events. + */ + private getTargetObject(targetObject?: TargetObject): TargetObject | null { + if (targetObject) { + return targetObject; + } + if (typeof window !== 'undefined') { + return window; + } + if (typeof document !== 'undefined') { + return document; + } + return null; + } + + /** + * Return the storage used. + */ + private getStorage(): StorageAbstract | undefined { + if (typeof window !== 'undefined') { + return new LocalStorage(window.localStorage); + } + return undefined; + } + + /** + * Setup the default connector from the storage. + */ + private async setDefaultConnector(): Promise { + const connectorName = + (await this._storage?.getItem(Fuel.STORAGE_KEY)) || this._connectors[0]?.name; + if (connectorName) { + // Setup all events for the current connector + return this.selectConnector(connectorName, { + emitEvents: false, + }); + } + + return undefined; + } + + /** + * Start listener for all the events of the current + * connector and emit them to the Fuel instance + */ + private setupConnectorEvents(events: string[]): void { + if (!this._currentConnector) { + return; + } + const currentConnector = this._currentConnector; + this._unsubscribes.map((unSub) => unSub()); + this._unsubscribes = events.map((event) => { + const handler = (...args: unknown[]) => this.emit(event, ...args); + currentConnector.on(event as FuelConnectorEventsType, handler); + return () => currentConnector.off(event, handler); + }); + } + + /** + * Call method from the current connector. + */ + private async callMethod(method: string, ...args: unknown[]) { + const hasConnector = await this.hasConnector(); + await this.pingConnector(); + if (!this._currentConnector || !hasConnector) { + throw new Error( + `No connector selected for calling ${method}. Use hasConnector before executing other methods.` + ); + } + if (typeof this._currentConnector[method as keyof FuelConnector] === 'function') { + return (this._currentConnector[method as keyof FuelConnector] as CallableFunction)(...args); + } + + return undefined; + } + + /** + * Create a method for each method proxy that is available on the Common interface + * and call the method from the current connector. + */ + private setupMethods(): void { + Object.values(FuelConnectorMethods).forEach((method) => { + this[method] = async (...args: unknown[]) => this.callMethod(method, ...args); + }); + } + + /** + * Fetch the status of a connector and set the installed and connected + * status. + */ + private async fetchConnectorStatus( + connector: FuelConnector & { _latestUpdate?: number } + ): Promise { + // Control fetch status to avoid rewriting the status + // on late responses in this way even if a response is + // late we can avoid rewriting the status of the connector + const requestTimestamp = Date.now(); + const [isConnected, ping] = await Promise.allSettled([ + withTimeout(connector.isConnected()), + withTimeout(this.pingConnector(connector)), + ]); + // If the requestTimestamp is greater than the latest update + // we can ignore the response as is treated as stale. + const isStale = requestTimestamp < (connector._latestUpdate || 0); + if (!isStale) { + // eslint-disable-next-line no-param-reassign + connector._latestUpdate = Date.now(); + // eslint-disable-next-line no-param-reassign + connector.installed = ping.status === 'fulfilled' && ping.value; + // eslint-disable-next-line no-param-reassign + connector.connected = isConnected.status === 'fulfilled' && isConnected.value; + } + return { + installed: connector.installed, + connected: connector.connected, + }; + } + + /** + * Fetch the status of all connectors and set the installed and connected + * status. + */ + private async fetchConnectorsStatus(): Promise { + return Promise.all( + this._connectors.map(async (connector) => this.fetchConnectorStatus(connector)) + ); + } + + /** + * Fetch the status of a connector and set the installed and connected + * status. If no connector is provided it will ping the current connector. + */ + private async pingConnector(connector?: FuelConnector) { + const curConnector = connector || this._currentConnector; + if (!curConnector) { + return false; + } + // If finds a ping in the cache and the value is true + // return from cache + try { + return await cacheFor(async () => withTimeout(curConnector.ping()), { + key: curConnector.name, + cache: this._pingCache, + cacheTime: PING_CACHE_TIME, + })(); + } catch { + throw new Error('Current connector is not available.'); + } + } + + /** + * Setup a listener for the FuelConnector event and add the connector + * to the list of new connectors. + */ + private setupConnectorListener = () => { + const { _targetObject: targetObject } = this; + const eventName = FuelConnectorEventType; + if (targetObject?.on) { + targetObject.on(eventName, this.addConnector); + return () => { + targetObject.off?.(eventName, this.addConnector); + }; + } + if (targetObject?.addEventListener) { + const handler = (e: CustomEvent) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.addConnector(e.detail); + }; + targetObject.addEventListener(eventName, handler); + return () => { + targetObject.removeEventListener?.(eventName, handler); + }; + } + return () => {}; + }; + + /** + * Add a new connector to the list of connectors. + */ + private addConnector = async (connector: FuelConnector): Promise => { + if (!this.getConnector(connector)) { + this._connectors.push(connector); + } + // Fetch the status of the new connector + await this.fetchConnectorStatus(connector); + // Emit connectors events once the connector list changes + this.emit(this.events.connectors, this._connectors); + // If the current connector is not set + if (!this._currentConnector) { + // set the new connector as currentConnector + await this.selectConnector(connector.name, { + emitEvents: false, + }); + } + }; + + private triggerConnectorEvents = async () => { + const [isConnected, networks, currentNetwork] = await Promise.all([ + this.isConnected(), + this.networks(), + this.currentNetwork(), + ]); + this.emit(this.events.connection, isConnected); + this.emit(this.events.networks, networks); + this.emit(this.events.currentNetwork, currentNetwork); + if (isConnected) { + const [accounts, currentAccount] = await Promise.all([ + this.accounts(), + this.currentAccount(), + ]); + this.emit(this.events.accounts, accounts); + this.emit(this.events.currentAccount, currentAccount); + } + }; + + /** + * Get a connector from the list of connectors. + */ + getConnector = (connector: FuelConnector | string): FuelConnector | null => + this._connectors.find((c) => { + const connectorName = typeof connector === 'string' ? connector : connector.name; + return c.name === connectorName || c === connector; + }) || null; + + /** + * Return the list of connectors with the status of installed and connected. + */ + async connectors(): Promise> { + await this.fetchConnectorsStatus(); + return this._connectors; + } + + /** + * Set the current connector to be used. + */ + async selectConnector( + connectorName: string, + options: FuelConnectorSelectOptions = { + emitEvents: true, + } + ): Promise { + const connector = this.getConnector(connectorName); + if (!connector) { + return false; + } + if (this._currentConnector?.name === connectorName) { + return true; + } + const { installed } = await this.fetchConnectorStatus(connector); + if (installed) { + this._currentConnector = connector; + this.emit(this.events.currentConnector, connector); + this.setupConnectorEvents(Object.values(FuelConnectorEventTypes)); + await this._storage?.setItem(Fuel.STORAGE_KEY, connector.name); + // If emitEvents is true we query all the data from the connector + // and emit the events to the Fuel instance allowing the application to + // react to changes in the connector state. + if (options.emitEvents) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.triggerConnectorEvents(); + } + return true; + } + return false; + } + + /** + * Return the current selected connector. + */ + currentConnector(): FuelConnector | null | undefined { + return this._currentConnector; + } + + /** + * Return true if any connector is available. + */ + async hasConnector(): Promise { + // If there is a current connector return true + // as the connector is ready + if (this._currentConnector) { + return true; + } + // If there is no current connector + // wait for the current connector to be set + // for 1 second and return false if is not set + const defer = deferPromise(); + this.once(this.events.currentConnector, () => { + defer.resolve(true); + }); + // As the max ping time is 1 second we wait for 2 seconds + // to allow applications to react to the current connector + return withTimeout(defer.promise, HAS_CONNECTOR_TIMEOUT) + .then(() => true) + .catch(() => false); + } + + async hasWallet(): Promise { + return this.hasConnector(); + } + + /** + * Return a Fuel Provider instance with extends features to work with + * connectors. + * + * @deprecated Provider is going to be deprecated in the future. + */ + async getProvider(providerOrNetwork?: Provider | Network): Promise { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn( + 'Get provider is deprecated, use getWallet instead. Provider is going to be removed in the future.' + ); + } + return this._getProvider(providerOrNetwork); + } + + /** + * Return a Fuel Provider instance with extends features to work with + * connectors. + */ + private async _getProvider(providerOrNetwork?: Provider | Network): Promise { + // Decide which provider to use based on the providerOrNetwork + let provider: Provider; + // If provider is a valid instance of a Provider use it + if (providerOrNetwork && 'getTransactionResponse' in providerOrNetwork) { + provider = providerOrNetwork; + // If the provided param is a valid network use it + } else if (providerOrNetwork && 'chainId' in providerOrNetwork && 'url' in providerOrNetwork) { + provider = await Provider.create(providerOrNetwork.url); + // If nor provider or network is provided use the current network + } else if (!providerOrNetwork) { + const currentNetwork = await this.currentNetwork(); + provider = await Provider.create(currentNetwork.url); + // If a provider or network was informed but is not valid + // throw an error + } else { + throw new Error('Provider is not valid.'); + } + return provider; + } + + /** + * Return a Fuel Wallet Locked instance with extends features to work with + * connectors. + */ + async getWallet( + address: string | AbstractAddress, + providerOrNetwork?: Provider | Network + ): Promise { + const provider = await this._getProvider(providerOrNetwork); + return new Account(address, provider, this); + } + + /** + * Remove all open listeners this is useful when you want to + * remove the Fuel instance and avoid memory leaks. + */ + unsubscribe(): void { + // Unsubscribe from all events + this._unsubscribes.map((unSub) => unSub()); + this._targetUnsubscribe(); + // Remove all listeners from fuel instance + this.removeAllListeners(); + } + + /** + * Clean all the data from the storage. + */ + async clean(): Promise { + await this._storage?.removeItem(Fuel.STORAGE_KEY); + } + + /** + * Removes all listeners and cleans the storage. + */ + async destroy(): Promise { + this.unsubscribe(); + await this.clean(); + } +} diff --git a/packages/account/src/connectors/index.ts b/packages/account/src/connectors/index.ts new file mode 100644 index 00000000000..0ee4be391d8 --- /dev/null +++ b/packages/account/src/connectors/index.ts @@ -0,0 +1,4 @@ +export * from './fuel'; +export * from './utils'; +export * from './types'; +export * from './fuel-connector'; diff --git a/packages/account/src/connectors/types/asset.ts b/packages/account/src/connectors/types/asset.ts new file mode 100644 index 00000000000..406cb0e4416 --- /dev/null +++ b/packages/account/src/connectors/types/asset.ts @@ -0,0 +1 @@ +export type { Asset, Fuel as AssetFuel, Ethereum as AssetEthereum } from '@fuels/assets'; diff --git a/packages/account/src/connectors/types/connector-metadata.ts b/packages/account/src/connectors/types/connector-metadata.ts new file mode 100644 index 00000000000..f9d09e35e2e --- /dev/null +++ b/packages/account/src/connectors/types/connector-metadata.ts @@ -0,0 +1,13 @@ +export type ConnectorMetadata = { + image?: + | string + | { + light: string; + dark: string; + }; + install: { + action: string; + link: string; + description: string; + }; +}; diff --git a/packages/account/src/connectors/types/connector-types.ts b/packages/account/src/connectors/types/connector-types.ts new file mode 100644 index 00000000000..ae74d4904e4 --- /dev/null +++ b/packages/account/src/connectors/types/connector-types.ts @@ -0,0 +1,42 @@ +export enum FuelConnectorMethods { + // General methods + ping = 'ping', + version = 'version', + // Connection methods + connect = 'connect', + disconnect = 'disconnect', + isConnected = 'isConnected', + // Account methods + accounts = 'accounts', + currentAccount = 'currentAccount', + // Signature methods + signMessage = 'signMessage', + sendTransaction = 'sendTransaction', + // Assets metadata methods + assets = 'assets', + addAsset = 'addAsset', + addAssets = 'addAssets', + // Network methods + networks = 'networks', + currentNetwork = 'currentNetwork', + addNetwork = 'addNetwork', + selectNetwork = 'selectNetwork', + // ABI methods + addABI = 'addABI', + getABI = 'getABI', + hasABI = 'hasABI', +} + +export enum FuelConnectorEventTypes { + connectors = 'connectors', + currentConnector = 'currentConnector', + connection = 'connection', + accounts = 'accounts', + currentAccount = 'currentAccount', + networks = 'networks', + currentNetwork = 'currentNetwork', + assets = 'assets', + abis = 'abis', +} + +export const FuelConnectorEventType = 'FuelConnector'; diff --git a/packages/account/src/connectors/types/constants.ts b/packages/account/src/connectors/types/constants.ts new file mode 100644 index 00000000000..ebd1a8666f1 --- /dev/null +++ b/packages/account/src/connectors/types/constants.ts @@ -0,0 +1,6 @@ +export const CONNECTOR_SCRIPT = 'FuelConnectorScript'; +export const CONTENT_SCRIPT_NAME = 'FuelContentScript'; +export const BACKGROUND_SCRIPT_NAME = 'FuelBackgroundScript'; +export const POPUP_SCRIPT_NAME = 'FuelPopUpScript'; +export const VAULT_SCRIPT_NAME = 'FuelVaultScript'; +export const EVENT_MESSAGE = 'message'; diff --git a/packages/account/src/connectors/types/data-type.ts b/packages/account/src/connectors/types/data-type.ts new file mode 100644 index 00000000000..128380c1d9e --- /dev/null +++ b/packages/account/src/connectors/types/data-type.ts @@ -0,0 +1,45 @@ +import type { JsonAbi } from '@fuel-ts/abi-coder'; + +/** + * @name Version + */ +export type Version = { + app: string; + /** + * Version selection this allow + * Caret Ranges ^1.2.3 ^0.2.5 ^0.0.4 + * Tilde Ranges ~1.2.3 ~1.2 ~1 + * And Exact Versions 1.0.0 + */ + network: string; +}; + +/** + * @name Network + */ +export type Network = { + /** + * The name of the network. + */ + url: string; + /** + * The chain id of the network. + */ + chainId: number; +}; + +/** + * ABI that represents a binary code interface from Sway. + * + * Read more at: https://docs.fuel.network/docs/specs/abi/json-abi-format/ + */ +export type FuelABI = JsonAbi; + +export enum MessageTypes { + ping = 'ping', + uiEvent = 'uiEvent', + event = 'event', + request = 'request', + response = 'response', + removeConnection = 'removeConnection', +} diff --git a/packages/account/src/connectors/types/events.ts b/packages/account/src/connectors/types/events.ts new file mode 100644 index 00000000000..4a3b72cdb8c --- /dev/null +++ b/packages/account/src/connectors/types/events.ts @@ -0,0 +1,174 @@ +import type { Asset } from '@fuels/assets'; +import type { JSONRPCRequest, JSONRPCResponse } from 'json-rpc-2.0'; + +import type { FuelConnector } from '../fuel-connector'; + +import type { FuelConnectorEventTypes } from './connector-types'; +import type { MessageTypes, Network } from './data-type'; +import type { MessageSender } from './message'; + +/** ** + * ======================================================================================== + * Helpers + * ======================================================================================== + */ + +/** + * Extract the event argument type from the event type. + */ +export type FuelEventArg = Extract< + FuelConnectorEventTypes, + { type: T } +>['data']; + +/** ** + * ======================================================================================== + * Events + * ======================================================================================== + */ + +export type BaseEvent = { + readonly target: string; + readonly connectorName?: string; + readonly id?: string; + readonly sender?: MessageSender; +} & T; + +export type UIEventMessage = BaseEvent<{ + readonly type: MessageTypes.uiEvent; + readonly ready: boolean; + readonly session: string; +}>; + +export type RequestMessage = BaseEvent<{ + readonly type: MessageTypes.request; + readonly request: JSONRPCRequest; +}>; + +export type ResponseMessage = BaseEvent<{ + readonly type: MessageTypes.response; + readonly response: JSONRPCResponse; +}>; + +export type EventMessageEvents = Array<{ + event: string; + params: Array; +}>; + +export type EventMessage = BaseEvent<{ + readonly type: MessageTypes.event; + readonly events: T; +}>; + +/** + * Event trigger when the accounts available to the + * connection changes. + * + * @property type - The event type. + * @property accounts - The accounts addresses + */ +export type AccountsEvent = { + type: FuelConnectorEventTypes.accounts; + data: Array; +}; + +/** + * Event trigger when the current account on the connector is changed + * if the account is not authorized for the connection it should trigger with value null. + * + * @property type - The event type. + * @property data - The current account selected or null. + */ +export type AccountEvent = { + type: FuelConnectorEventTypes.currentAccount; + data: string | null; +}; + +/** + * Event trigger when connection status changes. With the new connection status. + * + * @event ConnectionEvent + * @property type - The event type. + * @property data - The new connection status. + */ +export type ConnectionEvent = { + type: FuelConnectorEventTypes.connection; + data: boolean; +}; + +/** + * Event trigger when the network selected on the connector is changed. + * It should trigger even if the network is not available for the connection. + * + * @event NetworkEvent + * @property type - The event type. + * @property data - The network information + */ +export type NetworkEvent = { + type: FuelConnectorEventTypes.currentNetwork; + data: Network; +}; + +/** + * Event trigger when the network selected on the connector is changed. + * It should trigger even if the network is not available for the connection. + * + * @event NetworksEvent + * @property type - The event type. + * @property data - The network information + */ +export type NetworksEvent = { + type: FuelConnectorEventTypes.networks; + data: Network; +}; + +/** + * Event trigger when the list of connectors has changed. + * + * @event ConnectorsEvent + * @property type - The event type. + * @property data - The list of connectors + */ +export type ConnectorsEvent = { + type: FuelConnectorEventTypes.connectors; + data: Array; +}; + +/** + * Event trigger when the current connector has changed. + * + * @event ConnectorEvent + * @property type - The event type. + * @property data - The list of connectors + */ +export type ConnectorEvent = { + type: FuelConnectorEventTypes.currentConnector; + data: FuelConnector; +}; + +/** + * Event trigger when the assets list of metadata changed. + * + * @event AssetsEvent + * @property type - The event type. + * @property data - The list of assets + */ +export type AssetsEvent = { + type: FuelConnectorEventTypes.assets; + data: Array; +}; + +/** + * All the events available to the connector. + */ +export type FuelConnectorEvents = + | ConnectionEvent + | NetworkEvent + | NetworksEvent + | AccountEvent + | AccountsEvent + | ConnectorsEvent + | ConnectorEvent + | AssetsEvent; + +export type FuelConnectorEventsType = FuelConnectorEvents['type']; diff --git a/packages/account/src/connectors/types/index.ts b/packages/account/src/connectors/types/index.ts new file mode 100644 index 00000000000..b9cce13ba36 --- /dev/null +++ b/packages/account/src/connectors/types/index.ts @@ -0,0 +1,8 @@ +export * from './connector-metadata'; +export * from './connector-types'; +export * from './data-type'; +export * from './events'; +export * from './local-storage'; +export * from './target-object'; +export * from './message'; +export * from './asset'; diff --git a/packages/account/src/connectors/types/local-storage.ts b/packages/account/src/connectors/types/local-storage.ts new file mode 100644 index 00000000000..49a3403a6b6 --- /dev/null +++ b/packages/account/src/connectors/types/local-storage.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/require-await */ +import type { StorageAbstract } from '../../wallet-manager'; + +export class LocalStorage implements StorageAbstract { + private storage: Storage; + + constructor(localStorage: Storage) { + this.storage = localStorage; + } + + async setItem(key: string, value: string): Promise { + this.storage.setItem(key, value); + } + + async getItem(key: string): Promise { + return this.storage.getItem(key); + } + + async removeItem(key: string): Promise { + this.storage.removeItem(key); + } + + async clear(): Promise { + this.storage.clear(); + } +} diff --git a/packages/account/src/connectors/types/message.ts b/packages/account/src/connectors/types/message.ts new file mode 100644 index 00000000000..b2816eed521 --- /dev/null +++ b/packages/account/src/connectors/types/message.ts @@ -0,0 +1,16 @@ +import type { UIEventMessage, RequestMessage, ResponseMessage, EventMessage } from './events'; + +export interface MessageSender { + id?: string | undefined; + origin?: string | undefined; + tab?: { + id?: number | undefined; + index?: number | undefined; + windowId?: number | undefined; + url?: string | undefined; + title?: string | undefined; + favIconUrl?: string | undefined; + }; +} + +export type CommunicationMessage = UIEventMessage | RequestMessage | ResponseMessage | EventMessage; diff --git a/packages/account/src/connectors/types/target-object.ts b/packages/account/src/connectors/types/target-object.ts new file mode 100644 index 00000000000..5ed505b3b22 --- /dev/null +++ b/packages/account/src/connectors/types/target-object.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Target Object that represents the global event bus used by Fuel Connector Manager. + * On browser the default target is the window or document. For other environments + * the event bus should be provided. + */ +export interface TargetObject { + on?: (event: string, callback: any) => void; + off?: (event: string, callback: any) => void; + emit?: (event: string, data: any) => void; + addEventListener?: (event: string, callback: any) => void; + removeEventListener?: (event: string, callback: any) => void; + postMessage?: (message: string) => void; +} diff --git a/packages/account/src/connectors/utils/cache.ts b/packages/account/src/connectors/utils/cache.ts new file mode 100644 index 00000000000..f2a7f89f658 --- /dev/null +++ b/packages/account/src/connectors/utils/cache.ts @@ -0,0 +1,40 @@ +/* eslint-disable no-param-reassign */ + +export type CacheFor = { + [key: string]: { + timeout: number; + value: unknown; + } | null; +}; + +type CacheForOptions = { + key: string; + cache: CacheFor; + cacheTime: number; +}; + +export function cacheFor Promise>( + fn: F, + { cache, cacheTime, key }: CacheForOptions +): F { + return (async (...args: unknown[]) => { + if (cache[key] && cache[key]?.value) { + return cache[key]?.value as ReturnType; + } + clearTimeout(cache[key]?.timeout); + const result = await fn(...args); + + // Create cache auto clean + + cache[key] = { + timeout: Number( + setTimeout(() => { + cache[key] = null; + }, cacheTime) + ), + value: result, + }; + + return result; + }) as F; +} diff --git a/packages/account/src/connectors/utils/dispatch-fuel-connector-event.ts b/packages/account/src/connectors/utils/dispatch-fuel-connector-event.ts new file mode 100644 index 00000000000..0a56087bac5 --- /dev/null +++ b/packages/account/src/connectors/utils/dispatch-fuel-connector-event.ts @@ -0,0 +1,14 @@ +import type { FuelConnector } from '../fuel-connector'; +import { FuelConnectorEventType } from '../types'; + +/** + * Fuel Connector Event is a custom event that can be used by the connector to + * inform the Fuel Connector Manager that a new connector is available. + */ +export function dispatchFuelConnectorEvent(connector: FuelConnector) { + window.dispatchEvent( + new CustomEvent(FuelConnectorEventType, { + detail: connector, + }) + ); +} diff --git a/packages/account/src/connectors/utils/index.ts b/packages/account/src/connectors/utils/index.ts new file mode 100644 index 00000000000..475416a4509 --- /dev/null +++ b/packages/account/src/connectors/utils/index.ts @@ -0,0 +1,3 @@ +export * from './cache'; +export * from './dispatch-fuel-connector-event'; +export * from './promises'; diff --git a/packages/account/src/connectors/utils/promises.ts b/packages/account/src/connectors/utils/promises.ts new file mode 100644 index 00000000000..b1dc1993d13 --- /dev/null +++ b/packages/account/src/connectors/utils/promises.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type DeferPromise = { + promise: Promise; + resolve: (value: R) => void; + reject: (error: unknown) => void; +}; + +export function deferPromise() { + const defer: DeferPromise = {} as any; + + defer.promise = new Promise((resolve, reject) => { + defer.reject = reject; + defer.resolve = resolve; + }); + + return defer; +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function withTimeout, RT = Awaited>( + promise: F, + timeout: number = 1050 +): Promise { + const timeoutPromise = new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('Promise timed out')); + }, timeout); + }); + return Promise.race([timeoutPromise, promise]) as any; +} diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 1479514b604..7c470acebd4 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -7,3 +7,4 @@ export * from './signer'; export * from './wallet-manager'; export * from './predicate'; export * from './providers'; +export * from './connectors'; diff --git a/packages/account/src/predicate/predicate.ts b/packages/account/src/predicate/predicate.ts index 870ffe01ac4..96ce3ca2b1c 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -76,7 +76,9 @@ export class Predicate extends Account { request.inputs?.forEach((input) => { if (input.type === InputType.Coin && hexlify(input.owner) === this.address.toB256()) { + // eslint-disable-next-line no-param-reassign input.predicate = this.bytes; + // eslint-disable-next-line no-param-reassign input.predicateData = this.getPredicateData(policies.length); } }); diff --git a/packages/account/src/providers/provider.ts b/packages/account/src/providers/provider.ts index 3e94c0b14e9..7d111891cc7 100644 --- a/packages/account/src/providers/provider.ts +++ b/packages/account/src/providers/provider.ts @@ -657,6 +657,7 @@ export default class Provider { if (inputs) { inputs.forEach((input, index) => { if ('predicateGasUsed' in input && bn(input.predicateGasUsed).gt(0)) { + // eslint-disable-next-line no-param-reassign (transactionRequest.inputs[index]).predicateGasUsed = input.predicateGasUsed; } @@ -1359,4 +1360,9 @@ export default class Provider { }); return bn(latestBlockHeight); } + + // eslint-disable-next-line @typescript-eslint/require-await + async getTransactionResponse(transactionId: string): Promise { + return new TransactionResponse(transactionId, this); + } } diff --git a/packages/account/src/providers/utils/json.ts b/packages/account/src/providers/utils/json.ts index e8bbc3262e8..4819f992b05 100644 --- a/packages/account/src/providers/utils/json.ts +++ b/packages/account/src/providers/utils/json.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-param-reassign */ import { hexlify } from 'ethers'; import { clone } from 'ramda'; diff --git a/packages/account/test/fixtures/generate-accounts.ts b/packages/account/test/fixtures/generate-accounts.ts new file mode 100644 index 00000000000..ef615c241dd --- /dev/null +++ b/packages/account/test/fixtures/generate-accounts.ts @@ -0,0 +1,5 @@ +import { Address } from '@fuel-ts/address'; + +export function generateAccounts(total: number) { + return new Array(total).fill(0).map(() => Address.fromRandom().toString()); +} diff --git a/packages/account/test/fixtures/mocked-connector.ts b/packages/account/test/fixtures/mocked-connector.ts new file mode 100644 index 00000000000..d7698699e1e --- /dev/null +++ b/packages/account/test/fixtures/mocked-connector.ts @@ -0,0 +1,168 @@ +/* eslint-disable @typescript-eslint/require-await */ + +import { setTimeout } from 'timers/promises'; + +import type { + TransactionRequestLike, + WalletUnlocked, + Asset, + FuelABI, + ConnectorMetadata, + Network, +} from '../../src'; +import { FUEL_NETWORK_URL } from '../../src/configs'; +import { FuelConnector } from '../../src/connectors/fuel-connector'; +import { FuelConnectorEventTypes } from '../../src/connectors/types'; + +import { generateAccounts } from './generate-accounts'; + +type MockConnectorOptions = { + name?: string; + accounts?: Array; + networks?: Array; + wallets?: Array; + pingDelay?: number; + metadata?: Partial; +}; + +export class MockConnector extends FuelConnector { + _accounts: Array; + _networks: Array; + _wallets: Array; + _pingDelay: number; + name = 'Fuel Wallet'; + metadata: ConnectorMetadata = { + image: '/connectors/fuel-wallet.svg', + install: { + action: 'Install', + description: 'To connect your Fuel Wallet, install the browser extension.', + link: 'https://chrome.google.com/webstore/detail/fuel-wallet/dldjpboieedgcmpkchcjcbijingjcgok', + }, + }; + + constructor(options: MockConnectorOptions = {}) { + super(); + this._wallets = options.wallets ?? []; + if (options.wallets) { + this._accounts = options.wallets.map((w) => w.address.toString()); + } else { + this._accounts = options.accounts ?? generateAccounts(2); + } + this._networks = options.networks ?? [ + { + chainId: 0, + url: FUEL_NETWORK_URL, + }, + ]; + // Time should be under 1 second + this._pingDelay = options.pingDelay ?? 900; + this.name = options.name ?? this.name; + this.metadata = { + ...this.metadata, + ...options.metadata, + }; + } + + async ping() { + await setTimeout(this._pingDelay); + return true; + } + + async version() { + return { + app: '0.0.1', + network: '>=0.12.4', + }; + } + + async isConnected() { + return true; + } + + async accounts() { + return this._accounts; + } + + async connect() { + this.emit(FuelConnectorEventTypes.connection, true); + this.emit(FuelConnectorEventTypes.accounts, this._accounts); + this.emit(FuelConnectorEventTypes.currentAccount, this._accounts[0]); + return true; + } + + async disconnect() { + this.emit(FuelConnectorEventTypes.connection, false); + this.emit(FuelConnectorEventTypes.accounts, []); + this.emit(FuelConnectorEventTypes.currentAccount, null); + return false; + } + + async signMessage(_address: string, _message: string) { + const wallet = this._wallets.find((w) => w.address.toString() === _address); + if (!wallet) { + throw new Error('Wallet is not found!'); + } + return wallet.signMessage(_message); + } + + async sendTransaction(_address: string, _transaction: TransactionRequestLike) { + const wallet = this._wallets.find((w) => w.address.toString() === _address); + if (!wallet) { + throw new Error('Wallet is not found!'); + } + const { id } = await wallet.sendTransaction(_transaction); + return id; + } + + async currentAccount() { + return this._accounts[0]; + } + + async assets() { + return []; + } + + async addAsset(_asset: Asset) { + return true; + } + + async addAssets(_assets: Array) { + return true; + } + + async addNetwork(_network: string) { + const newNetwork = { + chainId: 0, + url: _network, + }; + this._networks.push(newNetwork); + this.emit(FuelConnectorEventTypes.networks, this._networks); + this.emit(FuelConnectorEventTypes.currentNetwork, newNetwork); + return true; + } + + async selectNetwork(_network: Network) { + this.emit(FuelConnectorEventTypes.currentNetwork, _network); + return true; + } + + async networks() { + return this._networks ?? []; + } + + async currentNetwork() { + return this._networks[0]; + } + + async addABI(_contractId: string, _abi: FuelABI) { + return true; + } + + async getABI(_id: string) { + return null; + } + + async hasABI(_id: string) { + return true; + } +} diff --git a/packages/account/test/fixtures/promise-callback.ts b/packages/account/test/fixtures/promise-callback.ts new file mode 100644 index 00000000000..3aef0cd85a9 --- /dev/null +++ b/packages/account/test/fixtures/promise-callback.ts @@ -0,0 +1,20 @@ +import type { Mock } from 'vitest'; + +import { deferPromise, type DeferPromise } from '../../src/connectors/utils'; + +export type PromiseCallback = Mock & { + promise: DeferPromise; +}; + +export function promiseCallback() { + const defer = deferPromise(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockFn: any = vi.fn(); + + mockFn.mockImplementation((...args: unknown[]) => { + defer.resolve(args); + }); + mockFn.promise = defer.promise; + + return mockFn as PromiseCallback; +} diff --git a/packages/account/test/fuel-wallet-connector.browser.test.ts b/packages/account/test/fuel-wallet-connector.browser.test.ts new file mode 100644 index 00000000000..f4e2eb56192 --- /dev/null +++ b/packages/account/test/fuel-wallet-connector.browser.test.ts @@ -0,0 +1,149 @@ +import type { StorageAbstract } from '../src'; +import { Fuel } from '../src/connectors/fuel'; +import { LocalStorage } from '../src/connectors/types'; +import { dispatchFuelConnectorEvent } from '../src/connectors/utils'; + +import { MockConnector } from './fixtures/mocked-connector'; +import { promiseCallback } from './fixtures/promise-callback'; + +/** + * @group browser + */ +describe('Fuel Connector on browser', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it('should add connector using window events', async () => { + const fuel = new Fuel({ + connectors: [], + storage: null, + }); + let connectors = await fuel.connectors(); + expect(connectors.length).toBe(0); + + // listen to connection event + const onConnectors = promiseCallback(); + fuel.on(fuel.events.connectors, onConnectors); + + // Trigger event to add connector + dispatchFuelConnectorEvent(new MockConnector()); + + // wait for the event to be triggered + await onConnectors.promise; + + connectors = await fuel.connectors(); + expect(onConnectors).toBeCalledTimes(1); + expect(onConnectors).toBeCalledWith(connectors); + expect(connectors.length).toBeGreaterThan(0); + expect(connectors[0].name).toEqual('Fuel Wallet'); + expect(connectors[0].installed).toEqual(true); + }); + + it('should retrieve default connector from storage', async () => { + const storage = new LocalStorage(window.localStorage); + + const walletConnector = new MockConnector({ + name: 'Fuel Wallet', + }); + const thirdPartyConnector = new MockConnector({ + name: 'Third Party Wallet', + }); + const fuel = new Fuel({ + connectors: [walletConnector, thirdPartyConnector], + storage, + }); + + // Select third party connector + await fuel.selectConnector(thirdPartyConnector.name); + + const fuelNewInstance = new Fuel({ + connectors: [walletConnector, thirdPartyConnector], + storage, + }); + await fuelNewInstance.hasConnector(); + expect(fuelNewInstance.currentConnector()?.name).toBe(thirdPartyConnector.name); + }); + + it('should use custom storage', async () => { + const storage = { + setItem: vi.fn(), + getItem: vi.fn(), + removeItem: vi.fn(), + } as unknown as StorageAbstract; + + const connector = new MockConnector(); + const fuel = new Fuel({ + connectors: [connector], + storage, + }); + + await fuel.hasConnector(); + expect(storage.getItem).toBeCalledTimes(1); + expect(storage.getItem).toBeCalledWith(Fuel.STORAGE_KEY); + expect(storage.setItem).toBeCalledTimes(1); + expect(storage.setItem).toBeCalledWith(Fuel.STORAGE_KEY, connector.name); + await fuel.clean(); + expect(storage.removeItem).toBeCalledTimes(1); + expect(storage.removeItem).toBeCalledWith(Fuel.STORAGE_KEY); + await fuel.destroy(); + expect(storage.removeItem).toBeCalledTimes(2); + expect(storage.removeItem).toBeCalledWith(Fuel.STORAGE_KEY); + }); + + it('should store on localStorage and remove on clean', async () => { + const connector = new MockConnector(); + const fuel = new Fuel({ + connectors: [connector], + }); + + await fuel.hasConnector(); + const value = window.localStorage.getItem(Fuel.STORAGE_KEY); + expect(value).toBeTruthy(); + expect(value).toEqual(connector.name); + await fuel.clean(); + const value2 = window.localStorage.getItem(Fuel.STORAGE_KEY); + expect(value2).toBeFalsy(); + }); + + it('should remove all listeners', async () => { + const connector = new MockConnector(); + const fuel = new Fuel({ + connectors: [connector], + }); + + await fuel.hasConnector(); + const onConnection = vi.fn(); + fuel.on(fuel.events.connection, onConnection); + + // Expect to be call + fuel.emit(fuel.events.connection, true); + connector.emit(fuel.events.connection, true); + expect(onConnection).toBeCalledTimes(2); + onConnection.mockClear(); + // Expect to not be called after cleaning + fuel.unsubscribe(); + fuel.emit(fuel.events.connection, true); + connector.emit(fuel.events.connection, true); + expect(onConnection).toBeCalledTimes(0); + }); + + it('should remove all listeners and clean storage on destroy', async () => { + const connector = new MockConnector(); + const fuel = new Fuel({ + connectors: [connector], + }); + + await fuel.hasConnector(); + const onConnection = vi.fn(); + fuel.on(fuel.events.connection, onConnection); + expect(window.localStorage.getItem(Fuel.STORAGE_KEY)).toBeTruthy(); + + // Expect to not be called after cleaning + await fuel.destroy(); + fuel.emit(fuel.events.connection, true); + connector.emit(fuel.events.connection, true); + expect(onConnection).toBeCalledTimes(0); + expect(window.localStorage.getItem(Fuel.STORAGE_KEY)).toBeFalsy(); + }); +}); diff --git a/packages/account/test/fuel-wallet-connector.test.ts b/packages/account/test/fuel-wallet-connector.test.ts new file mode 100644 index 00000000000..5f6a1cbc6d9 --- /dev/null +++ b/packages/account/test/fuel-wallet-connector.test.ts @@ -0,0 +1,550 @@ +import { Address } from '@fuel-ts/address'; +import { BaseAssetId } from '@fuel-ts/address/configs'; +import type { AbstractAddress, BytesLike } from '@fuel-ts/interfaces'; +import type { BN } from '@fuel-ts/math'; +import { bn } from '@fuel-ts/math'; +import { EventEmitter } from 'events'; + +import type { ProviderOptions } from '../src'; +import { FUEL_NETWORK_URL } from '../src/configs'; +import { Fuel } from '../src/connectors/fuel'; +import { FuelConnectorEventType } from '../src/connectors/types'; +import { Provider, TransactionStatus } from '../src/providers'; +import { Wallet } from '../src/wallet'; + +import { MockConnector } from './fixtures/mocked-connector'; +import { promiseCallback } from './fixtures/promise-callback'; + +/** + * @group node + * @group browser + */ +describe('Fuel Connector', () => { + it('should ensure is instantiated using default connectors', async () => { + const fuel = new Fuel(); + const connectors = await fuel.connectors(); + expect(connectors.length).toBe(0); + }); + + it('should be instantiaded using devMode connectors when flag is informed', async () => { + const fuel = new Fuel({ + devMode: true, + }); + const connectors = await fuel.connectors(); + expect(connectors.length).toBe(0); + }); + + it('should add connector using event of a custom EventBus', async () => { + const eventBus = new EventEmitter(); + const fuel = new Fuel({ + targetObject: eventBus, + connectors: [], + storage: null, + }); + let connectors = await fuel.connectors(); + expect(connectors.length).toBe(0); + + // listen to connection event + const onConnectors = promiseCallback(); + fuel.on(fuel.events.connectors, onConnectors); + + // Trigger event to add connector + eventBus.emit(FuelConnectorEventType, new MockConnector()); + // wait for the event to be triggered + await onConnectors.promise; + + connectors = await fuel.connectors(); + expect(onConnectors).toBeCalledTimes(1); + expect(onConnectors).toBeCalledWith(connectors); + expect(connectors.length).toBeGreaterThan(0); + expect(connectors[0].name).toEqual('Fuel Wallet'); + expect(connectors[0].installed).toEqual(true); + }); + + it('should ensure hasConnector works just fine', async () => { + let fuel = new Fuel({ + connectors: [new MockConnector()], + storage: null, + }); + let hasConnector = await fuel.hasConnector(); + expect(hasConnector).toBeTruthy(); + + fuel = new Fuel({ + connectors: [], + storage: null, + }); + hasConnector = await fuel.hasConnector(); + expect(hasConnector).toBeFalsy(); + }); + + it('should ensure isConnected works just fine', async () => { + const fuel = new Fuel({ + connectors: [new MockConnector()], + storage: null, + }); + const isConnected = await fuel.isConnected(); + expect(isConnected).toBeTruthy(); + }); + + it('should ensure isConnected works just fine', async () => { + const fuel = new Fuel({ + connectors: [new MockConnector()], + storage: null, + }); + const isConnected = await fuel.ping(); + expect(isConnected).toBeTruthy(); + }); + + it('should ensure connect works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + + // listen to connection event + const onConnection = promiseCallback(); + const onAccounts = promiseCallback(); + const onCurrentAccount = promiseCallback(); + fuel.on(fuel.events.connection, onConnection); + fuel.on(fuel.events.accounts, onAccounts); + fuel.on(fuel.events.currentAccount, onCurrentAccount); + + const isConnected = await fuel.connect(); + expect(isConnected).toBeTruthy(); + const accounts = await fuel.accounts(); + await onConnection.promise; + await onAccounts.promise; + await onCurrentAccount.promise; + + expect(onConnection).toBeCalledTimes(1); + expect(onConnection).toBeCalledWith(true); + expect(onAccounts).toBeCalledTimes(1); + expect(onAccounts).toBeCalledWith(accounts); + expect(onCurrentAccount).toBeCalledTimes(1); + expect(onCurrentAccount).toBeCalledWith(accounts[0]); + }); + + it('should ensure disconnect works just fine', async () => { + const fuel = new Fuel({ + connectors: [new MockConnector()], + }); + + // listen to connection event + const onConnection = vi.fn(); + const onAccounts = vi.fn(); + const onCurrentAccount = vi.fn(); + fuel.on(fuel.events.connection, onConnection); + fuel.on(fuel.events.accounts, onAccounts); + fuel.on(fuel.events.currentAccount, onCurrentAccount); + + const isConnected = await fuel.disconnect(); + expect(isConnected).toBeFalsy(); + expect(onConnection).toBeCalledTimes(1); + expect(onConnection).toBeCalledWith(false); + expect(onAccounts).toBeCalledWith([]); + expect(onCurrentAccount).toBeCalledWith(null); + }); + + it('should ensure accounts returns all connected accounts', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const accounts = await fuel.accounts(); + expect(accounts.length).toBeGreaterThan(0); + }); + + it('should ensure currentAccount returns current account', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const [account] = await fuel.accounts(); + const currentAccount = await fuel.currentAccount(); + expect(currentAccount).toEqual(account); + }); + + it('should ensure networks returns all networks', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const networks = await fuel.networks(); + expect(networks.length).toBeGreaterThan(0); + }); + + it('should ensure currentNetwork returns current network', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const [network] = await fuel.networks(); + const currentNetwork = await fuel.currentNetwork(); + expect(currentNetwork).toEqual(network); + }); + + it('should ensure addNetwork works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const networkUrl = 'https://beta-5.fuel.network'; + const newNetwork = { + url: networkUrl, + chainId: 0, + }; + + // listen to connection event + const onNetworks = vi.fn(); + const onCurrentNetwork = vi.fn(); + fuel.on(fuel.events.networks, onNetworks); + fuel.on(fuel.events.currentNetwork, onCurrentNetwork); + + const isNetworkAdded = await fuel.addNetwork(networkUrl); + const networks = await fuel.networks(); + expect(isNetworkAdded).toEqual(true); + expect(networks).toContainEqual(newNetwork); + expect(onNetworks).toBeCalledTimes(1); + expect(onNetworks).toBeCalledWith(networks); + expect(onCurrentNetwork).toBeCalledTimes(1); + expect(onCurrentNetwork).toBeCalledWith(newNetwork); + }); + + it('should ensure selectNetwork works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const newNetwork = { + url: 'https://beta-5.fuel.network', + chainId: 0, + }; + + // listen to connection event + const onCurrentNetwork = vi.fn(); + fuel.on(fuel.events.currentNetwork, onCurrentNetwork); + + const networkHasSwitch = await fuel.selectNetwork(newNetwork); + expect(networkHasSwitch).toEqual(true); + expect(onCurrentNetwork).toBeCalledTimes(1); + expect(onCurrentNetwork).toBeCalledWith(newNetwork); + }); + + it('should ensure addAsset works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const isAdded = await fuel.addAsset({ + name: 'Asset', + symbol: 'AST', + icon: 'ast.png', + networks: [ + { + type: 'fuel', + assetId: BaseAssetId, + decimals: 9, + chainId: 0, + }, + ], + }); + expect(isAdded).toEqual(true); + }); + + it('should ensure addAssets works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const isAdded = await fuel.addAssets([ + { + name: 'Asset', + symbol: 'AST', + icon: 'ast.png', + networks: [ + { + type: 'fuel', + assetId: BaseAssetId, + decimals: 9, + chainId: 0, + }, + ], + }, + ]); + expect(isAdded).toEqual(true); + }); + + it('should ensure assets returns all assets', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const assets = await fuel.assets(); + expect(assets.length).toEqual(0); + }); + + it('should ensure addABI works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const isAdded = await fuel.addABI('0x001123', { + types: [], + loggedTypes: [], + functions: [], + configurables: [], + }); + expect(isAdded).toEqual(true); + }); + + it('should ensure getABI works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const abi = await fuel.getABI('0x001123'); + expect(abi).toStrictEqual(null); + }); + + it('should ensure hasABI works just fine', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [new MockConnector()], + }); + const hasFuel = await fuel.hasABI('0x001123'); + expect(hasFuel).toBeTruthy(); + }); + + it('should throw if ping takes more than a second', async () => { + const fuel = new Fuel({ + storage: null, + connectors: [ + new MockConnector({ + pingDelay: 2000, + }), + ], + }); + await expect(fuel.connect()).rejects.toThrowError(); + }); + + it('should ensure getWallet return an wallet', async () => { + const provider = await Provider.create(FUEL_NETWORK_URL); + + const wallets = [ + Wallet.fromPrivateKey( + '0x1ff16505df75735a5bcf4cb4cf839903120c181dd9be6781b82cda23543bd242', + provider + ), + ]; + const network = { + chainId: await provider.getChainId(), + url: provider.url, + }; + const fuel = new Fuel({ + storage: null, + connectors: [ + new MockConnector({ + wallets, + networks: [network], + }), + ], + }); + const account = await fuel.currentAccount(); + if (!account) { + throw new Error('Account not found'); + } + const wallet = await fuel.getWallet(account); + expect(wallet.provider.url).toEqual(network.url); + const receiver = Wallet.fromAddress(Address.fromRandom(), provider); + const response = await wallet.transfer(receiver.address, bn(1000), BaseAssetId, { + gasPrice: bn(1), + gasLimit: bn(100_000), + }); + const { status } = await response.waitForResult(); + expect(status).toEqual(TransactionStatus.success); + expect((await receiver.getBalance()).toString()).toEqual('1000'); + }, 10_000); + + it('should be able to have switch between connectors', async () => { + const thirdPartyConnectorName = 'Third Party Wallet'; + const walletConnectorName = 'Fuel Wallet'; + const fuel = new Fuel({ + storage: null, + connectors: [ + new MockConnector({ + name: walletConnectorName, + }), + new MockConnector({ + accounts: [], + name: thirdPartyConnectorName, + }), + ], + }); + + // Connectors should be available + const connectors = await fuel.connectors(); + expect(connectors.length).toEqual(2); + expect(connectors[0].name).toEqual(walletConnectorName); + expect(connectors[1].name).toEqual(thirdPartyConnectorName); + + // Switch between connectors + await fuel.selectConnector(walletConnectorName); + expect(fuel.currentConnector()?.name).toBe(walletConnectorName); + expect(await fuel.accounts()).toHaveLength(2); + + await fuel.selectConnector(thirdPartyConnectorName); + expect(fuel.currentConnector()?.name).toBe(thirdPartyConnectorName); + expect(await fuel.accounts()).toHaveLength(0); + }); + + it('should trigger currentConnector and other events when switch connector', async () => { + const walletConnector = new MockConnector({ + name: 'Fuel Wallet', + networks: [ + { + chainId: 0, + url: 'https://wallet.fuel.network', + }, + ], + }); + const thirdPartyConnector = new MockConnector({ + name: 'Third Party Wallet', + networks: [ + { + chainId: 1, + url: 'https://thridy.fuel.network', + }, + ], + }); + const fuel = new Fuel({ + storage: null, + connectors: [walletConnector, thirdPartyConnector], + }); + + async function expectEventsForConnector(connector: MockConnector) { + const onCurrentConnector = promiseCallback(); + const onConnection = promiseCallback(); + const onAccounts = promiseCallback(); + const onNetworks = promiseCallback(); + const onCurrentNetwork = promiseCallback(); + const onCurrentAccount = promiseCallback(); + fuel.on(fuel.events.currentConnector, onCurrentConnector); + fuel.on(fuel.events.connection, onConnection); + fuel.on(fuel.events.accounts, onAccounts); + fuel.on(fuel.events.networks, onNetworks); + fuel.on(fuel.events.currentNetwork, onCurrentNetwork); + fuel.on(fuel.events.currentAccount, onCurrentAccount); + + await fuel.selectConnector(connector.name); + await Promise.all([ + onCurrentConnector.promise, + onConnection.promise, + onAccounts.promise, + onNetworks.promise, + onCurrentNetwork.promise, + onCurrentAccount.promise, + ]); + + expect(onCurrentConnector).toBeCalledTimes(1); + expect(onCurrentConnector).toBeCalledWith(fuel.getConnector(connector.name)); + expect(onConnection).toBeCalledTimes(1); + expect(onConnection).toBeCalledWith(true); + expect(onAccounts).toBeCalledTimes(1); + expect(onAccounts).toBeCalledWith(connector._accounts); + expect(onNetworks).toBeCalledTimes(1); + expect(onNetworks).toBeCalledWith(connector._networks); + expect(onCurrentNetwork).toBeCalledTimes(1); + expect(onCurrentNetwork).toBeCalledWith(connector._networks[0]); + expect(onCurrentAccount).toBeCalledTimes(1); + expect(onCurrentAccount).toBeCalledWith(connector._accounts[0]); + } + + await fuel.hasConnector(); + await expectEventsForConnector(thirdPartyConnector); + await expectEventsForConnector(walletConnector); + }); + + it('should only proxy events from the currentConnector', async () => { + const walletConnector = new MockConnector({ + name: 'Fuel Wallet', + networks: [ + { + chainId: 0, + url: 'https://wallet.fuel.network', + }, + ], + }); + const thirdPartyConnector = new MockConnector({ + name: 'Third Party Wallet', + networks: [ + { + chainId: 1, + url: 'https://thridy.fuel.network', + }, + ], + }); + const fuel = new Fuel({ + storage: null, + connectors: [walletConnector, thirdPartyConnector], + }); + + // Select wallet connector + await fuel.selectConnector(walletConnector.name); + // Select third party connector + await fuel.selectConnector(thirdPartyConnector.name); + // Select wallet connector + await fuel.selectConnector(walletConnector.name); + + // Ensure that the current connector is the wallet connector + expect(fuel.currentConnector()?.name).toBe(walletConnector.name); + + const onAccounts = promiseCallback(); + fuel.on(fuel.events.accounts, onAccounts); + + // Should not call event with third party connector + thirdPartyConnector.emit(fuel.events.accounts, thirdPartyConnector._accounts); + expect(onAccounts).toBeCalledTimes(0); + + // Should trigger event from the current connector + walletConnector.emit(fuel.events.accounts, walletConnector._accounts); + expect(onAccounts).toBeCalledTimes(1); + expect(onAccounts).toBeCalledWith(walletConnector._accounts); + }); + + it('should be able to getWallet with custom provider', async () => { + const defaultProvider = await Provider.create(FUEL_NETWORK_URL); + + const defaultWallet = Wallet.generate({ + provider: defaultProvider, + }); + const connector = new MockConnector({ + wallets: [defaultWallet], + }); + const fuel = new Fuel({ + connectors: [connector], + }); + + class CustomProvider extends Provider { + static async create(_url: string, opts?: ProviderOptions) { + const provider = new CustomProvider(FUEL_NETWORK_URL, opts); + await provider.fetchChainAndNodeInfo(); + return provider; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async getBalance(_owner: AbstractAddress, _assetId: BytesLike = BaseAssetId): Promise { + return bn(1234); + } + } + + const currentAccount = await fuel.currentAccount(); + if (!currentAccount) { + throw new Error('Account not found'); + } + + const provider = await CustomProvider.create(FUEL_NETWORK_URL); + const wallet = await fuel.getWallet(currentAccount, provider); + expect(wallet.provider).toBeInstanceOf(CustomProvider); + expect(await wallet.getBalance()).toEqual(bn(1234)); + }); +}); diff --git a/packages/errors/src/error-codes.ts b/packages/errors/src/error-codes.ts index e02c67180eb..71a2416ffc7 100644 --- a/packages/errors/src/error-codes.ts +++ b/packages/errors/src/error-codes.ts @@ -35,6 +35,7 @@ export enum ErrorCode { INSUFFICIENT_BALANCE = 'insufficient-balance', WALLET_MANAGER_ERROR = 'wallet-manager-error', HD_WALLET_ERROR = 'hd-wallet-error', + MISSING_CONNECTOR = 'missing-connector', // errors PARSE_FAILED = 'parse-failed', diff --git a/packages/fuels/src/cli/commands/deploy/deployContract.ts b/packages/fuels/src/cli/commands/deploy/deployContract.ts index 5a0d29da328..5166998e895 100644 --- a/packages/fuels/src/cli/commands/deploy/deployContract.ts +++ b/packages/fuels/src/cli/commands/deploy/deployContract.ts @@ -18,6 +18,7 @@ export async function deployContract( if (existsSync(storageSlotsPath)) { const storageSlots = JSON.parse(readFileSync(storageSlotsPath, 'utf-8')); + // eslint-disable-next-line no-param-reassign deployConfig.storageSlots = storageSlots; } @@ -26,6 +27,7 @@ export async function deployContract( const abi = JSON.parse(readFileSync(abiPath, 'utf-8')); const contractFactory = new ContractFactory(bytecode, abi, wallet); + // eslint-disable-next-line no-param-reassign deployConfig.gasPrice = deployConfig.gasPrice ?? gasPrice; const contract = await contractFactory.deployContract(deployConfig); diff --git a/packages/fuels/src/cli/commands/dev/autoStartFuelCore.ts b/packages/fuels/src/cli/commands/dev/autoStartFuelCore.ts index 936f7f17812..ef7176a79c4 100644 --- a/packages/fuels/src/cli/commands/dev/autoStartFuelCore.ts +++ b/packages/fuels/src/cli/commands/dev/autoStartFuelCore.ts @@ -59,7 +59,9 @@ export const autoStartFuelCore = async (config: FuelsConfig) => { killChildProcess: cleanup, }; + // eslint-disable-next-line no-param-reassign config.providerUrl = fuelCore.providerUrl; + // eslint-disable-next-line no-param-reassign config.privateKey = defaultConsensusKey; } diff --git a/packages/program/src/functions/base-invocation-scope.ts b/packages/program/src/functions/base-invocation-scope.ts index 06f0ec41252..d0790887652 100644 --- a/packages/program/src/functions/base-invocation-scope.ts +++ b/packages/program/src/functions/base-invocation-scope.ts @@ -408,6 +408,7 @@ export class BaseInvocationScope { const { gasLimit, gasPrice } = transactionRequest; if (!gasLimitSpecified) { + // eslint-disable-next-line no-param-reassign transactionRequest.gasLimit = gasUsed; } else if (gasLimit.lt(gasUsed)) { throw new FuelError( @@ -417,6 +418,7 @@ export class BaseInvocationScope { } if (!gasPriceSpecified) { + // eslint-disable-next-line no-param-reassign transactionRequest.gasPrice = minGasPrice; } else if (gasPrice.lt(minGasPrice)) { throw new FuelError( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90e87a99532..735da5475bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -556,12 +556,18 @@ importers: '@fuel-ts/versions': specifier: workspace:* version: link:../versions + '@fuels/assets': + specifier: ^0.1.4 + version: 0.1.4 '@fuels/vm-asm': specifier: 0.42.1 version: 0.42.1 '@noble/curves': specifier: ^1.3.0 version: 1.3.0 + dexie-observable: + specifier: 4.0.1-beta.13 + version: 4.0.1-beta.13(dexie@3.2.4) ethers: specifier: ^6.7.1 version: 6.7.1 @@ -577,6 +583,9 @@ importers: graphql-tag: specifier: ^2.12.6 version: 2.12.6(graphql@16.6.0) + json-rpc-2.0: + specifier: ^1.7.0 + version: 1.7.0 portfinder: specifier: ^1.0.32 version: 1.0.32 @@ -595,7 +604,7 @@ importers: devDependencies: '@graphql-codegen/cli': specifier: ^2.13.7 - version: 2.13.7(@babel/core@7.23.3)(@types/node@20.10.5)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.2.2) + version: 2.13.7(@babel/core@7.23.3)(@types/node@20.11.13)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.2.2) '@graphql-codegen/typescript': specifier: ^2.8.0 version: 2.8.0(graphql@16.6.0) @@ -4208,11 +4217,15 @@ packages: resolution: {integrity: sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@fuels/assets@0.1.4: + resolution: {integrity: sha512-6rSbzxxDlDkoW9u4vvSbTfnA4Cy9WjZ1ajBOuKTnvoQ9EzQKvrP4lYpN2SjqdfJkdhRfCItaf1aQq/avaN51BQ==} + dev: false + /@fuels/vm-asm@0.42.1: resolution: {integrity: sha512-5e0IDHen26hrKc93ejYNDhQFbqi+EQ7xPpFJcUnSrz0+6zPdPhA2dtwh5UqN0fYDM5AcEFd0wpq+r7Pd2XS5AQ==} dev: false - /@graphql-codegen/cli@2.13.7(@babel/core@7.23.3)(@types/node@20.10.5)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.2.2): + /@graphql-codegen/cli@2.13.7(@babel/core@7.23.3)(@types/node@20.11.13)(graphql@16.6.0)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-Rpk4WWrDgkDoVELftBr7/74MPiYmCITEF2+AWmyZZ2xzaC9cO2PqzZ+OYDEBNWD6UEk0RrIfVSa+slDKjhY59w==} hasBin: true peerDependencies: @@ -4226,23 +4239,23 @@ packages: '@graphql-tools/apollo-engine-loader': 7.3.26(graphql@16.6.0) '@graphql-tools/code-file-loader': 7.3.23(@babel/core@7.23.3)(graphql@16.6.0) '@graphql-tools/git-loader': 7.3.0(@babel/core@7.23.3)(graphql@16.6.0) - '@graphql-tools/github-loader': 7.3.28(@babel/core@7.23.3)(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/github-loader': 7.3.28(@babel/core@7.23.3)(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/graphql-file-loader': 7.5.17(graphql@16.6.0) '@graphql-tools/json-file-loader': 7.4.18(graphql@16.6.0) '@graphql-tools/load': 7.8.14(graphql@16.6.0) - '@graphql-tools/prisma-loader': 7.2.72(@types/node@20.10.5)(graphql@16.6.0) - '@graphql-tools/url-loader': 7.17.18(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/prisma-loader': 7.2.72(@types/node@20.11.13)(graphql@16.6.0) + '@graphql-tools/url-loader': 7.17.18(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/utils': 8.13.1(graphql@16.6.0) '@whatwg-node/fetch': 0.3.2 ansi-escapes: 4.3.2 chalk: 4.1.2 chokidar: 3.5.3 cosmiconfig: 7.1.0 - cosmiconfig-typescript-loader: 4.1.1(@types/node@20.10.5)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.2.2) + cosmiconfig-typescript-loader: 4.1.1(@types/node@20.11.13)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.2.2) debounce: 1.2.1 detect-indent: 6.1.0 graphql: 16.6.0 - graphql-config: 4.3.6(@types/node@20.10.5)(graphql@16.6.0)(typescript@5.2.2) + graphql-config: 4.3.6(@types/node@20.11.13)(graphql@16.6.0)(typescript@5.2.2) inquirer: 8.2.5 is-glob: 4.0.3 json-to-pretty-yaml: 1.2.2 @@ -4486,7 +4499,7 @@ packages: - utf-8-validate dev: true - /@graphql-tools/executor-http@0.1.10(@types/node@20.10.5)(graphql@16.6.0): + /@graphql-tools/executor-http@0.1.10(@types/node@20.11.13)(graphql@16.6.0): resolution: {integrity: sha512-hnAfbKv0/lb9s31LhWzawQ5hghBfHS+gYWtqxME6Rl0Aufq9GltiiLBcl7OVVOnkLF0KhwgbYP1mB5VKmgTGpg==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -4497,7 +4510,7 @@ packages: dset: 3.1.2 extract-files: 11.0.0 graphql: 16.6.0 - meros: 1.3.0(@types/node@20.10.5) + meros: 1.3.0(@types/node@20.11.13) tslib: 2.6.0 value-or-promise: 1.0.12 transitivePeerDependencies: @@ -4550,13 +4563,13 @@ packages: - supports-color dev: true - /@graphql-tools/github-loader@7.3.28(@babel/core@7.23.3)(@types/node@20.10.5)(graphql@16.6.0): + /@graphql-tools/github-loader@7.3.28(@babel/core@7.23.3)(@types/node@20.11.13)(graphql@16.6.0): resolution: {integrity: sha512-OK92Lf9pmxPQvjUNv05b3tnVhw0JRfPqOf15jZjyQ8BfdEUrJoP32b4dRQQem/wyRL24KY4wOfArJNqzpsbwCA==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/executor-http': 0.1.10(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/executor-http': 0.1.10(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/graphql-tag-pluck': 7.5.2(@babel/core@7.23.3)(graphql@16.6.0) '@graphql-tools/utils': 9.2.1(graphql@16.6.0) '@whatwg-node/fetch': 0.8.8 @@ -4588,10 +4601,10 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - '@babel/parser': 7.22.5 + '@babel/parser': 7.23.9 '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.23.3) - '@babel/traverse': 7.22.5 - '@babel/types': 7.22.5 + '@babel/traverse': 7.23.4 + '@babel/types': 7.23.9 '@graphql-tools/utils': 9.2.1(graphql@16.6.0) graphql: 16.6.0 tslib: 2.6.0 @@ -4654,12 +4667,12 @@ packages: tslib: 2.6.0 dev: true - /@graphql-tools/prisma-loader@7.2.72(@types/node@20.10.5)(graphql@16.6.0): + /@graphql-tools/prisma-loader@7.2.72(@types/node@20.11.13)(graphql@16.6.0): resolution: {integrity: sha512-0a7uV7Fky6yDqd0tI9+XMuvgIo6GAqiVzzzFV4OSLry4AwiQlI3igYseBV7ZVOGhedOTqj/URxjpiv07hRcwag==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - '@graphql-tools/url-loader': 7.17.18(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/url-loader': 7.17.18(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/utils': 9.2.1(graphql@16.6.0) '@types/js-yaml': 4.0.5 '@types/json-stable-stringify': 1.0.34 @@ -4712,7 +4725,7 @@ packages: value-or-promise: 1.0.12 dev: true - /@graphql-tools/url-loader@7.17.18(@types/node@20.10.5)(graphql@16.6.0): + /@graphql-tools/url-loader@7.17.18(@types/node@20.11.13)(graphql@16.6.0): resolution: {integrity: sha512-ear0CiyTj04jCVAxi7TvgbnGDIN2HgqzXzwsfcqiVg9cvjT40NcMlZ2P1lZDgqMkZ9oyLTV8Bw6j+SyG6A+xPw==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -4720,7 +4733,7 @@ packages: '@ardatan/sync-fetch': 0.0.1 '@graphql-tools/delegate': 9.0.35(graphql@16.6.0) '@graphql-tools/executor-graphql-ws': 0.0.14(graphql@16.6.0) - '@graphql-tools/executor-http': 0.1.10(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/executor-http': 0.1.10(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/executor-legacy-ws': 0.0.11(graphql@16.6.0) '@graphql-tools/utils': 9.2.1(graphql@16.6.0) '@graphql-tools/wrap': 9.4.2(graphql@16.6.0) @@ -5774,7 +5787,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 dev: true @@ -6366,7 +6379,7 @@ packages: /@types/bn.js@5.1.1: resolution: {integrity: sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==} dependencies: - '@types/node': 16.18.34 + '@types/node': 20.10.5 dev: false /@types/body-parser@1.19.2: @@ -6593,13 +6606,13 @@ packages: /@types/mkdirp@1.0.2: resolution: {integrity: sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==} dependencies: - '@types/node': 16.18.34 + '@types/node': 20.10.5 dev: true /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 16.18.34 + '@types/node': 20.10.5 form-data: 3.0.1 dev: true @@ -6613,6 +6626,7 @@ packages: /@types/node@16.18.34: resolution: {integrity: sha512-VmVm7gXwhkUimRfBwVI1CHhwp86jDWR04B5FGebMMyxV90SlCmFujwUHrxTD4oO+SOYU86SoxvhgeRQJY7iXFg==} + dev: false /@types/node@18.15.13: resolution: {integrity: sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==} @@ -6626,6 +6640,12 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.13: + resolution: {integrity: sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -6639,7 +6659,7 @@ packages: /@types/prompts@2.4.9: resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} dependencies: - '@types/node': 16.18.34 + '@types/node': 20.10.5 kleur: 3.0.3 dev: true @@ -6696,7 +6716,7 @@ packages: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} dependencies: '@types/glob': 8.1.0 - '@types/node': 16.18.34 + '@types/node': 20.10.5 dev: true /@types/scheduler@0.16.3: @@ -7777,11 +7797,6 @@ packages: engines: {node: '>=0.4.0'} dev: false - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} - engines: {node: '>=0.4.0'} - dev: true - /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -7797,7 +7812,6 @@ packages: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /acorn@8.9.0: resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} @@ -9478,7 +9492,7 @@ packages: '@iarna/toml': 2.2.5 dev: true - /cosmiconfig-typescript-loader@4.1.1(@types/node@20.10.5)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.2.2): + /cosmiconfig-typescript-loader@4.1.1(@types/node@20.11.13)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-9DHpa379Gp0o0Zefii35fcmuuin6q92FnLDffzdZ0l9tVd3nEobG3O+MZ06+kuBvFTSVScvNb/oHA13Nd4iipg==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -9487,13 +9501,13 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.13 cosmiconfig: 7.0.1 - ts-node: 10.9.1(@types/node@20.10.5)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.11.13)(typescript@5.2.2) typescript: 5.2.2 dev: true - /cosmiconfig-typescript-loader@4.1.1(@types/node@20.10.5)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.2.2): + /cosmiconfig-typescript-loader@4.1.1(@types/node@20.11.13)(cosmiconfig@7.1.0)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-9DHpa379Gp0o0Zefii35fcmuuin6q92FnLDffzdZ0l9tVd3nEobG3O+MZ06+kuBvFTSVScvNb/oHA13Nd4iipg==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -9502,9 +9516,9 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.13 cosmiconfig: 7.1.0 - ts-node: 10.9.1(@types/node@20.10.5)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.11.13)(typescript@5.2.2) typescript: 5.2.2 dev: true @@ -10215,6 +10229,19 @@ packages: resolution: {integrity: sha512-Ctp4hInA0BEavlUoRy9mhGq0i+JSo/AwVyX2EFgZmV1kYB+Zq+EMBAn52QWu6FbRr10hRb6pBl420upbp4++vg==} dev: true + /dexie-observable@4.0.1-beta.13(dexie@3.2.4): + resolution: {integrity: sha512-axmgPk7yjoPwj+0DdQIE5s1MBXi+6XcIFIjBKdQAmSGpsLQSth/LHvMOQ3q3Wj6pwIE5hqIxg2GL75sVqQbhEw==} + peerDependencies: + dexie: ^3.0.2 || ^4.0.1-alpha.5 + dependencies: + dexie: 3.2.4 + dev: false + + /dexie@3.2.4: + resolution: {integrity: sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==} + engines: {node: '>=6.0'} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -12644,7 +12671,7 @@ packages: /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - /graphql-config@4.3.6(@types/node@20.10.5)(graphql@16.6.0)(typescript@5.2.2): + /graphql-config@4.3.6(@types/node@20.11.13)(graphql@16.6.0)(typescript@5.2.2): resolution: {integrity: sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==} engines: {node: '>= 10.0.0'} peerDependencies: @@ -12654,15 +12681,15 @@ packages: '@graphql-tools/json-file-loader': 7.4.18(graphql@16.6.0) '@graphql-tools/load': 7.8.14(graphql@16.6.0) '@graphql-tools/merge': 8.4.2(graphql@16.6.0) - '@graphql-tools/url-loader': 7.17.18(@types/node@20.10.5)(graphql@16.6.0) + '@graphql-tools/url-loader': 7.17.18(@types/node@20.11.13)(graphql@16.6.0) '@graphql-tools/utils': 8.13.1(graphql@16.6.0) cosmiconfig: 7.0.1 cosmiconfig-toml-loader: 1.0.0 - cosmiconfig-typescript-loader: 4.1.1(@types/node@20.10.5)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.2.2) + cosmiconfig-typescript-loader: 4.1.1(@types/node@20.11.13)(cosmiconfig@7.0.1)(ts-node@10.9.1)(typescript@5.2.2) graphql: 16.6.0 minimatch: 4.2.1 string-env-interpolation: 1.0.1 - ts-node: 10.9.1(@types/node@20.10.5)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.11.13)(typescript@5.2.2) tslib: 2.6.0 transitivePeerDependencies: - '@swc/core' @@ -14425,7 +14452,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.9.0 + acorn: 8.11.3 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -14494,6 +14521,10 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-rpc-2.0@1.7.0: + resolution: {integrity: sha512-asnLgC1qD5ytP+fvBP8uL0rvj+l8P6iYICbzZ8dVxCpESffVjzA7KkYkbKCIbavs7cllwH1ZUaNtJwphdeRqpg==} + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -15207,7 +15238,7 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - /meros@1.3.0(@types/node@20.10.5): + /meros@1.3.0(@types/node@20.11.13): resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} engines: {node: '>=13'} peerDependencies: @@ -15216,7 +15247,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.13 dev: true /methods@1.1.2: @@ -19815,7 +19846,7 @@ packages: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true - /ts-node@10.9.1(@types/node@20.10.5)(typescript@5.2.2): + /ts-node@10.9.1(@types/node@20.11.13)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -19834,9 +19865,9 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.5 - acorn: 8.9.0 - acorn-walk: 8.2.0 + '@types/node': 20.11.13 + acorn: 8.11.3 + acorn-walk: 8.3.2 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 diff --git a/tsdoc.base.json b/tsdoc.base.json index a7e80d93aa2..5dda73401da 100644 --- a/tsdoc.base.json +++ b/tsdoc.base.json @@ -104,6 +104,18 @@ { "tagName": "@hideGroups", "syntaxKind": "modifier" + }, + { + "tagName": "@name", + "syntaxKind": "modifier" + }, + { + "tagName": "@emits", + "syntaxKind": "modifier" + }, + { + "tagName": "@type", + "syntaxKind": "modifier" } ] } diff --git a/vite.browser.config.mts b/vite.browser.config.mts index 51b1dbc3bc1..fb5742c1082 100644 --- a/vite.browser.config.mts +++ b/vite.browser.config.mts @@ -12,7 +12,7 @@ const config: UserConfig = { Buffer: true, global: true, }, - include: ["fs", "crypto", "buffer", "fs"], + include: ["fs", "crypto", "buffer", "fs", "events", "timers/promises"], overrides: { fs: "memfs", }, @@ -20,6 +20,7 @@ const config: UserConfig = { ], optimizeDeps: { exclude: ["fsevents"], + include: ["events", "timers/promises"], }, test: { coverage: {