"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamrClient = void 0;
require("reflect-metadata");
require("./utils/PatchTsyringe");
const utils_1 = require("@streamr/utils");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const merge_1 = __importDefault(require("lodash/merge"));
const omit_1 = __importDefault(require("lodash/omit"));
const tsyringe_1 = require("tsyringe");
const Authentication_1 = require("./Authentication");
const Config_1 = require("./Config");
const DestroySignal_1 = require("./DestroySignal");
const Ethereum_1 = require("./Ethereum");
const Message_1 = require("./Message");
const MetricsPublisher_1 = require("./MetricsPublisher");
const NetworkNodeFacade_1 = require("./NetworkNodeFacade");
const StreamIDBuilder_1 = require("./StreamIDBuilder");
const StreamrClientError_1 = require("./StreamrClientError");
const LocalGroupKeyStore_1 = require("./encryption/LocalGroupKeyStore");
const PublisherKeyExchange_1 = require("./encryption/PublisherKeyExchange");
const events_1 = require("./events");
const Publisher_1 = require("./publish/Publisher");
const OperatorRegistry_1 = require("./contracts/OperatorRegistry");
const StorageNodeRegistry_1 = require("./contracts/StorageNodeRegistry");
const StreamRegistry_1 = require("./contracts/StreamRegistry");
const StreamStorageRegistry_1 = require("./contracts/StreamStorageRegistry");
const MessageStream_1 = require("./subscribe/MessageStream");
const Resends_1 = require("./subscribe/Resends");
const Subscriber_1 = require("./subscribe/Subscriber");
const Subscription_1 = require("./subscribe/Subscription");
const resendSubscription_1 = require("./subscribe/resendSubscription");
const waitForStorage_1 = require("./subscribe/waitForStorage");
const LoggerFactory_1 = require("./utils/LoggerFactory");
const promises_1 = require("./utils/promises");
const utils_2 = require("./utils/utils");
const ERC1271ContractFacade_1 = require("./contracts/ERC1271ContractFacade");
const ContractFactory_1 = require("./ContractFactory");
/**
 * The main API used to interact with Streamr.
 *
 * @category Important
 */
class StreamrClient {
    static generateEthereumAccount = Ethereum_1.generateEthereumAccount;
    id;
    publisher;
    subscriber;
    resends;
    node;
    streamRegistry;
    streamStorageRegistry;
    storageNodeRegistry;
    operatorRegistry;
    localGroupKeyStore;
    streamIdBuilder;
    config;
    authentication;
    eventEmitter;
    destroySignal;
    loggerFactory;
    constructor(config = {}, 
    /** @internal */
    parentContainer = tsyringe_1.container) {
        const strictConfig = (0, Config_1.createStrictConfig)(config);
        const authentication = (0, Authentication_1.createAuthentication)(strictConfig);
        (0, Config_1.redactConfig)(strictConfig);
        const container = parentContainer.createChildContainer();
        container.register(Authentication_1.AuthenticationInjectionToken, { useValue: authentication });
        container.register(Config_1.ConfigInjectionToken, { useValue: strictConfig });
        container.register(ERC1271ContractFacade_1.InstantiateERC1271ContractsToken, {
            useValue: (0, ERC1271ContractFacade_1.createNewInstantiateContractsFn)(container.resolve(ContractFactory_1.ContractFactory), strictConfig)
        });
        // eslint-disable-next-line max-len
        container.register(utils_1.TheGraphClient, { useValue: (0, utils_2.createTheGraphClient)(container.resolve(events_1.StreamrClientEventEmitter), strictConfig) });
        this.id = strictConfig.id;
        this.config = strictConfig;
        this.authentication = authentication;
        this.publisher = container.resolve(Publisher_1.Publisher);
        this.subscriber = container.resolve(Subscriber_1.Subscriber);
        this.resends = container.resolve(Resends_1.Resends);
        this.node = container.resolve(NetworkNodeFacade_1.NetworkNodeFacade);
        this.streamRegistry = container.resolve(StreamRegistry_1.StreamRegistry);
        this.streamStorageRegistry = container.resolve(StreamStorageRegistry_1.StreamStorageRegistry);
        this.storageNodeRegistry = container.resolve(StorageNodeRegistry_1.StorageNodeRegistry);
        this.operatorRegistry = container.resolve(OperatorRegistry_1.OperatorRegistry);
        this.localGroupKeyStore = container.resolve(LocalGroupKeyStore_1.LocalGroupKeyStore);
        this.streamIdBuilder = container.resolve(StreamIDBuilder_1.StreamIDBuilder);
        this.eventEmitter = container.resolve(events_1.StreamrClientEventEmitter);
        this.destroySignal = container.resolve(DestroySignal_1.DestroySignal);
        this.loggerFactory = container.resolve(LoggerFactory_1.LoggerFactory);
        container.resolve(PublisherKeyExchange_1.PublisherKeyExchange); // side effect: activates publisher key exchange
        container.resolve(MetricsPublisher_1.MetricsPublisher); // side effect: activates metrics publisher
    }
    // --------------------------------------------------------------------------------------------
    // Publish
    // --------------------------------------------------------------------------------------------
    /**
     * Publishes a message to a stream partition in the network.
     *
     * @category Important
     *
     * @param streamDefinition - the stream or stream partition to publish the message to
     * @param content - the content (the payload) of the message (must be JSON serializable)
     * @param metadata - provide additional metadata to be included in the message or to control the publishing process
     * @returns the published message (note: the field {@link Message.content} is encrypted if the stream is private)
     */
    async publish(streamDefinition, content, metadata) {
        const result = await this.publisher.publish(streamDefinition, content, metadata);
        this.eventEmitter.emit('publish', result);
        return (0, Message_1.convertStreamMessageToMessage)(result);
    }
    /**
     * Manually updates the encryption key used when publishing messages to a given stream.
     */
    async updateEncryptionKey(opts) {
        if (opts.streamId === undefined) {
            throw new Error('streamId required');
        }
        if (opts.key !== undefined && this.config.encryption.litProtocolEnabled) {
            throw new StreamrClientError_1.StreamrClientError('cannot pass "key" when Lit Protocol is enabled', 'UNSUPPORTED_OPERATION');
        }
        const streamId = await this.streamIdBuilder.toStreamID(opts.streamId);
        const queue = await this.publisher.getGroupKeyQueue(streamId);
        if (opts.distributionMethod === 'rotate') {
            await queue.rotate(opts.key);
        }
        else if (opts.distributionMethod === 'rekey') {
            await queue.rekey(opts.key);
        }
        else {
            throw new Error(`assertion failed: distribution method ${opts.distributionMethod}`);
        }
    }
    /**
     * Adds an encryption key for a given publisher to the key store.
     *
     * @remarks Keys will be added to the store automatically by the client as encountered. This method can be used to
     * manually add some known keys into the store.
     */
    async addEncryptionKey(key, publisherId) {
        await this.localGroupKeyStore.set(key.id, publisherId, key.data);
    }
    // --------------------------------------------------------------------------------------------
    // Subscribe
    // --------------------------------------------------------------------------------------------
    /**
     * Subscribes to a stream partition in the network.
     *
     * @category Important
     *
     * @param options - the stream or stream partition to subscribe to,
     * additionally a resend can be performed by providing resend options
     * @param onMessage - callback will be invoked for each message received in subscription
     * @returns a {@link Subscription} that can be used to manage the subscription etc.
     */
    async subscribe(options, onMessage) {
        if ((options.raw === true) && (options.resend !== undefined)) {
            throw new Error('Raw subscriptions are not supported for resend');
        }
        const streamPartId = await this.streamIdBuilder.toStreamPartID(options);
        const eventEmitter = new eventemitter3_1.default();
        const sub = new Subscription_1.Subscription(streamPartId, options.raw ?? false, options.erc1271Contract !== undefined ? (0, utils_1.toEthereumAddress)(options.erc1271Contract) : undefined, eventEmitter, this.loggerFactory);
        if (options.resend !== undefined) {
            (0, resendSubscription_1.initResendSubscription)(sub, options.resend, this.resends, (streamId) => this.streamStorageRegistry.getStorageNodes(streamId), this.config, eventEmitter, this.loggerFactory);
        }
        await this.subscriber.add(sub);
        if (onMessage !== undefined) {
            sub.useLegacyOnMessageHandler(onMessage);
        }
        this.eventEmitter.emit('subscribe', undefined);
        return sub;
    }
    /**
     * Unsubscribes from streams or stream partitions in the network.
     *
     * @remarks no-op if subscription does not exist
     *
     * @category Important
     *
     * @param streamDefinitionOrSubscription - leave as `undefined` to unsubscribe from all existing subscriptions.
     */
    async unsubscribe(streamDefinitionOrSubscription) {
        if (streamDefinitionOrSubscription instanceof Subscription_1.Subscription) {
            const sub = streamDefinitionOrSubscription;
            return this.subscriber.remove(sub);
        }
        else {
            const subs = await this.getSubscriptions(streamDefinitionOrSubscription);
            return Promise.allSettled(subs.map((sub) => this.subscriber.remove(sub)));
        }
    }
    /**
     * Returns a list of subscriptions matching the given criteria.
     *
     * @category Important
     *
     * @param streamDefinition - leave as `undefined` to get all subscriptions
     */
    async getSubscriptions(streamDefinition) {
        const matcher = (streamDefinition !== undefined)
            ? await this.streamIdBuilder.getMatcher(streamDefinition)
            : () => true;
        return this.subscriber.getSubscriptions().filter((s) => matcher(s.streamPartId));
    }
    // --------------------------------------------------------------------------------------------
    // Resend
    // --------------------------------------------------------------------------------------------
    /**
     * Performs a resend of stored historical data.
     *
     * @category Important
     *
     * @param streamDefinition - the stream partition for which data should be resent
     * @param options - defines the kind of resend that should be performed
     * @param onMessage - callback will be invoked for each message retrieved
     * @returns a {@link MessageStream} that provides an alternative way of iterating messages. Rejects if the stream is
     * not stored (i.e. is not assigned to a storage node).
     */
    async resend(streamDefinition, options, onMessage) {
        const streamPartId = await this.streamIdBuilder.toStreamPartID(streamDefinition);
        const getStorageNodes = (streamId) => this.streamStorageRegistry.getStorageNodes(streamId);
        const pipeline = await this.resends.resend(streamPartId, options, getStorageNodes);
        const messageStream = new MessageStream_1.MessageStream(pipeline);
        if (onMessage !== undefined) {
            messageStream.useLegacyOnMessageHandler(onMessage);
        }
        return messageStream;
    }
    /**
     * Waits for a message to be stored by a storage node.
     *
     * @param message - the message to be awaited for
     * @param options - additional options for controlling waiting and message matching
     * @returns rejects if message was found in storage before timeout
     */
    waitForStorage(message, options) {
        const defaultOptions = {
            // eslint-disable-next-line no-underscore-dangle
            interval: this.config._timeouts.storageNode.retryInterval,
            // eslint-disable-next-line no-underscore-dangle
            timeout: this.config._timeouts.storageNode.timeout,
            count: 100
        };
        return (0, waitForStorage_1.waitForStorage)(message, (0, merge_1.default)(defaultOptions, options), this.resends, this.streamStorageRegistry);
    }
    // --------------------------------------------------------------------------------------------
    // Stream management
    // --------------------------------------------------------------------------------------------
    /**
     * Gets a stream.
     *
     * @category Important
     *
     * @returns rejects if the stream is not found
     */
    async getStream(streamIdOrPath) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        return this.streamRegistry.getStream(streamId, false);
    }
    /**
     * Creates a new stream.
     *
     * @category Important
     *
     * @param propsOrStreamIdOrPath - the stream id to be used for the new stream, and optionally, any
     * associated metadata
     *
     * @remarks when creating a stream with an ENS domain, the returned promise can take several minutes to settle
     */
    async createStream(propsOrStreamIdOrPath) {
        const props = typeof propsOrStreamIdOrPath === 'object' ? propsOrStreamIdOrPath : { id: propsOrStreamIdOrPath };
        const streamId = await this.streamIdBuilder.toStreamID(props.id);
        return this.streamRegistry.createStream(streamId, (0, merge_1.default)({ partitions: 1 }, (0, omit_1.default)(props, 'id')));
    }
    /**
     * Gets a stream, creating one if it does not exist.
     *
     * @category Important
     *
     * @param props - the stream id to get or create. Field `partitions` is only used if creating the stream.
     *
     * @remarks when creating a stream with an ENS domain, the returned promise can take several minutes to settle
     */
    async getOrCreateStream(props) {
        try {
            return await this.getStream(props.id);
        }
        catch (err) {
            if (err.code === 'STREAM_NOT_FOUND') {
                return this.createStream(props);
            }
            throw err;
        }
    }
    /**
     * Updates the metadata of a stream.
     *
     * @param props - the stream id and the metadata fields to be updated
     */
    async updateStream(props) {
        const streamId = await this.streamIdBuilder.toStreamID(props.id);
        return this.streamRegistry.updateStream(streamId, (0, omit_1.default)(props, 'id'));
    }
    /**
     * Deletes a stream.
     */
    deleteStream(streamIdOrPath) {
        return this.streamRegistry.deleteStream(streamIdOrPath);
    }
    /**
     * Searches for streams based on given criteria.
     *
     * @param term - a search term that should be part of the stream id of a result
     * @param permissionFilter - permissions that should be in effect for a result
     * @param orderBy - the default is ascending order by stream id field
     */
    searchStreams(term, permissionFilter, orderBy = { field: 'id', direction: 'asc' }) {
        return this.streamRegistry.searchStreams(term, permissionFilter, orderBy);
    }
    // --------------------------------------------------------------------------------------------
    // Permissions
    // --------------------------------------------------------------------------------------------
    /**
     * Gets all ethereum addresses that have {@link StreamPermission.PUBLISH} permission to the stream.
     */
    getStreamPublishers(streamIdOrPath) {
        return this.streamRegistry.getStreamPublishers(streamIdOrPath);
    }
    /**
     * Gets all ethereum addresses that have {@link StreamPermission.SUBSCRIBE} permission to the stream.
     */
    getStreamSubscribers(streamIdOrPath) {
        return this.streamRegistry.getStreamSubscribers(streamIdOrPath);
    }
    /**
     * Checks whether the given permission is in effect.
     */
    hasPermission(query) {
        return this.streamRegistry.hasPermission(query);
    }
    /**
     * Returns the list of all permissions in effect for a given stream.
     */
    getPermissions(streamIdOrPath) {
        return this.streamRegistry.getPermissions(streamIdOrPath);
    }
    /**
     * Grants permissions on a given stream.
     */
    grantPermissions(streamIdOrPath, ...assignments) {
        return this.streamRegistry.grantPermissions(streamIdOrPath, ...assignments);
    }
    /**
     * Revokes permissions on a given stream.
     */
    revokePermissions(streamIdOrPath, ...assignments) {
        return this.streamRegistry.revokePermissions(streamIdOrPath, ...assignments);
    }
    /**
     * Sets a list of permissions to be in effect.
     *
     * @remarks Can be used to set the permissions of multiple streams in one transaction. Great for doing bulk
     * operations and saving gas costs. Notice that the behaviour is overwriting, therefore any existing permissions not
     * defined will be removed (per stream).
     */
    setPermissions(...items) {
        return this.streamRegistry.setPermissions(...items);
    }
    /**
     * Checks whether a given ethereum address has {@link StreamPermission.PUBLISH} permission to a stream.
     */
    async isStreamPublisher(streamIdOrPath, userAddress) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        return this.streamRegistry.isStreamPublisher(streamId, (0, utils_1.toEthereumAddress)(userAddress), false);
    }
    /**
     * Checks whether a given ethereum address has {@link StreamPermission.SUBSCRIBE} permission to a stream.
     */
    async isStreamSubscriber(streamIdOrPath, userAddress) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        return this.streamRegistry.isStreamSubscriber(streamId, (0, utils_1.toEthereumAddress)(userAddress), false);
    }
    // --------------------------------------------------------------------------------------------
    // Storage
    // --------------------------------------------------------------------------------------------
    /**
     * Assigns a stream to a storage node.
     */
    async addStreamToStorageNode(streamIdOrPath, storageNodeAddress) {
        return this.streamStorageRegistry.addStreamToStorageNode(streamIdOrPath, (0, utils_1.toEthereumAddress)(storageNodeAddress));
    }
    /**
     * Unassigns a stream from a storage node.
     */
    async removeStreamFromStorageNode(streamIdOrPath, storageNodeAddress) {
        return this.streamStorageRegistry.removeStreamFromStorageNode(streamIdOrPath, (0, utils_1.toEthereumAddress)(storageNodeAddress));
    }
    /**
     * Checks whether a stream is assigned to a storage node.
     */
    async isStoredStream(streamIdOrPath, storageNodeAddress) {
        return this.streamStorageRegistry.isStoredStream(streamIdOrPath, (0, utils_1.toEthereumAddress)(storageNodeAddress));
    }
    /**
     * Gets all streams assigned to a storage node.
     *
     * @returns a list of {@link Stream} as well as `blockNumber` of result (i.e. blockchain state)
     */
    async getStoredStreams(storageNodeAddress) {
        return this.streamStorageRegistry.getStoredStreams((0, utils_1.toEthereumAddress)(storageNodeAddress));
    }
    /**
     * Gets a list of storage nodes.
     *
     * @param streamIdOrPath - if a stream is given, returns the list of storage nodes the stream has been assigned to;
     * leave as `undefined` to return all storage nodes
     */
    async getStorageNodes(streamIdOrPath) {
        return this.streamStorageRegistry.getStorageNodes(streamIdOrPath);
    }
    /**
     * Sets the metadata of a storage node in the storage node registry.
     *
     * @remarks Acts on behalf of the wallet associated with the current {@link StreamrClient} instance.
     *
     * @param metadata - if `undefined`, removes the storage node from the registry
     */
    setStorageNodeMetadata(metadata) {
        return this.storageNodeRegistry.setStorageNodeMetadata(metadata);
    }
    /**
     * Gets the metadata of a storage node from the storage node registry.
     *
     * @returns rejects if the storage node is not found
     */
    async getStorageNodeMetadata(nodeAddress) {
        return this.storageNodeRegistry.getStorageNodeMetadata((0, utils_1.toEthereumAddress)(nodeAddress));
    }
    // --------------------------------------------------------------------------------------------
    // Authentication
    // --------------------------------------------------------------------------------------------
    /**
     * Gets the Signer associated with the current {@link StreamrClient} instance.
     */
    getSigner() {
        return this.authentication.getStreamRegistryChainSigner();
    }
    /**
     * Gets the Ethereum address of the wallet associated with the current {@link StreamrClient} instance.
     */
    getAddress() {
        return this.authentication.getAddress();
    }
    // --------------------------------------------------------------------------------------------
    // Network node
    // --------------------------------------------------------------------------------------------
    /**
     * @deprecated This in an internal method
     */
    getNode() {
        return this.node.getNode();
    }
    async inspect(node, streamDefinition) {
        const streamPartId = await this.streamIdBuilder.toStreamPartID(streamDefinition);
        // TODO: right now if the node is not joined to the stream partition, the below will return false instantly.
        // It would be better if it actually joined the stream partition for us (and maybe left when we are done?).
        return this.node.inspect(node, streamPartId);
    }
    async setProxies(streamDefinition, nodes, direction, connectionCount) {
        const streamPartId = await this.streamIdBuilder.toStreamPartID(streamDefinition);
        await this.node.setProxies(streamPartId, nodes, direction, connectionCount);
    }
    /**
     * Used to set known entry points for a stream partition. If entry points are not set they
     * will be automatically discovered from the Streamr Network.
    */
    async setStreamPartitionEntryPoints(streamDefinition, entryPoints) {
        const streamPartId = await this.streamIdBuilder.toStreamPartID(streamDefinition);
        await this.node.setStreamPartEntryPoints(streamPartId, entryPoints);
    }
    // --------------------------------------------------------------------------------------------
    // Lifecycle
    // --------------------------------------------------------------------------------------------
    /**
     * Used to manually initialize the network stack and connect to the network.
     *
     * @remarks Connecting is handled automatically by the client. Generally this method need not be called by the user.
     */
    connect() {
        // eslint-disable-next-line no-underscore-dangle
        return this._connect();
    }
    _connect = (0, promises_1.pOnce)(async () => {
        await this.node.startNode();
    });
    /**
     * Destroys an instance of a {@link StreamrClient} by disconnecting from peers, clearing any pending tasks, and
     * freeing up resources. This should be called once a user is done with the instance.
     *
     * @remarks As the name implies, the client instance (or any streams or subscriptions returned by it) should _not_
     * be used after calling this method.
     */
    destroy() {
        // eslint-disable-next-line no-underscore-dangle
        return this._destroy();
    }
    _destroy = (0, promises_1.pOnce)(async () => {
        this.eventEmitter.removeAllListeners();
        // eslint-disable-next-line no-underscore-dangle
        this._connect.reset(); // reset connect (will error on next call)
        const tasks = [
            this.destroySignal.destroy().then(() => undefined),
            this.unsubscribe()
        ];
        await Promise.allSettled(tasks);
        await Promise.all(tasks);
    });
    async getPeerDescriptor() {
        return (0, utils_2.convertPeerDescriptorToNetworkPeerDescriptor)((await this.node.getNode()).getPeerDescriptor());
    }
    /**
     * Get the network-level node id of the client.
     */
    async getNodeId() {
        return this.node.getNodeId();
    }
    /**
     * Get diagnostic info about the underlying network. Useful for debugging issues.
     *
     * @remark returned object's structure can change without semver considerations
     */
    async getDiagnosticInfo() {
        return (await this.node.getNode()).getDiagnosticInfo();
    }
    /**
     * @deprecated This in an internal method
     */
    getConfig() {
        return this.config;
    }
    /**
     * Get overrides for transaction options. Use as a parameter when submitting
     * transactions via ethers library.
     */
    getEthersOverrides() {
        return (0, Ethereum_1.getEthersOverrides)(this.config);
    }
    // --------------------------------------------------------------------------------------------
    // Events
    // --------------------------------------------------------------------------------------------
    /**
     * Adds an event listener to the client.
     * @param eventName - event name, see {@link StreamrClientEvents} for options
     * @param listener - the callback function
     */
    on(eventName, listener) {
        this.eventEmitter.on(eventName, listener);
    }
    /**
     * Adds an event listener to the client that is invoked only once.
     * @param eventName - event name, see {@link StreamrClientEvents} for options
     * @param listener - the callback function
     */
    once(eventName, listener) {
        this.eventEmitter.once(eventName, listener);
    }
    /**
     * Removes an event listener from the client.
     * @param eventName - event name, see {@link StreamrClientEvents} for options
     * @param listener - the callback function to remove
     */
    off(eventName, listener) {
        this.eventEmitter.off(eventName, listener);
    }
}
exports.StreamrClient = StreamrClient;
//# sourceMappingURL=StreamrClient.js.map