"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var StreamRegistry_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamRegistry = void 0;
const network_contracts_1 = require("@streamr/network-contracts");
const utils_1 = require("@streamr/utils");
const ethers_1 = require("ethers");
const intersection_1 = __importDefault(require("lodash/intersection"));
const tsyringe_1 = require("tsyringe");
const Identity_1 = require("../identity/Identity");
const Config_1 = require("../Config");
const RpcProviderSource_1 = require("../RpcProviderSource");
const StreamIDBuilder_1 = require("../StreamIDBuilder");
const StreamMetadata_1 = require("../StreamMetadata");
const StreamrClientError_1 = require("../StreamrClientError");
const ethereumUtils_1 = require("../ethereumUtils");
const events_1 = require("../events");
const permission_1 = require("../permission");
const GeneratorUtils_1 = require("../utils/GeneratorUtils");
const LoggerFactory_1 = require("../utils/LoggerFactory");
const Mapping_1 = require("../utils/Mapping");
const ChainEventPoller_1 = require("./ChainEventPoller");
const ContractFactory_1 = require("./ContractFactory");
const contract_1 = require("./contract");
const searchStreams_1 = require("./searchStreams");
const validatePermissionAssignments = (assignments) => {
    for (const assignment of assignments) {
        // In the StreamRegistry v5 contract, these permissions can only be assigned to users
        // who have EthereumAddress as their userId. Also public permission is not allowed
        // for these users.
        const ADMIN_PERMISSION_TYPES = [permission_1.StreamPermission.EDIT, permission_1.StreamPermission.DELETE, permission_1.StreamPermission.GRANT];
        const adminPermissions = (0, intersection_1.default)(assignment.permissions, ADMIN_PERMISSION_TYPES);
        if (adminPermissions.length > 0) {
            const createError = (prefix) => {
                return new StreamrClientError_1.StreamrClientError(`${prefix} is not supported for permission types: ${adminPermissions.map((p) => p.toUpperCase()).join(', ')}`, 'UNSUPPORTED_OPERATION');
            };
            if ((0, permission_1.isPublicPermissionAssignment)(assignment)) {
                throw createError('Public permission');
            }
            else if (!(0, utils_1.isEthereumAddressUserId)(assignment.userId)) {
                throw createError('Non-Ethereum user id');
            }
        }
    }
};
const streamContractErrorProcessor = (err, streamId, registry) => {
    if (err.reason?.code === 'CALL_EXCEPTION') {
        throw new StreamrClientError_1.StreamrClientError('Stream not found: id=' + streamId, 'STREAM_NOT_FOUND');
    }
    else {
        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
        throw new Error(`Could not reach the ${registry} Smart Contract: ${err.message}`);
    }
};
const invalidateCache = (cache, streamId) => {
    cache.invalidate((key) => {
        const cachedStreamId = Array.isArray(key) ? key[0] : key;
        return cachedStreamId === streamId;
    });
};
let StreamRegistry = StreamRegistry_1 = class StreamRegistry {
    streamRegistryContract;
    streamRegistryContractReadonly;
    contractFactory;
    rpcProviderSource;
    theGraphClient;
    streamIdBuilder;
    /** @internal */
    config;
    identity;
    logger;
    metadataCache;
    publisherCache;
    subscriberCache;
    publicSubscribePermissionCache;
    /** @internal */
    constructor(contractFactory, rpcProviderSource, chainEventPoller, theGraphClient, streamIdBuilder, config, identity, eventEmitter, loggerFactory) {
        this.contractFactory = contractFactory;
        this.rpcProviderSource = rpcProviderSource;
        this.theGraphClient = theGraphClient;
        this.streamIdBuilder = streamIdBuilder;
        this.config = config;
        this.identity = identity;
        this.logger = loggerFactory.createLogger(module);
        this.streamRegistryContractReadonly = this.contractFactory.createReadContract((0, utils_1.toEthereumAddress)(this.config.contracts.streamRegistryChainAddress), network_contracts_1.StreamRegistryABI, this.rpcProviderSource.getProvider(), 'streamRegistry');
        (0, contract_1.initContractEventGateway)({
            sourceDefinition: {
                contractInterfaceFragment: new ethers_1.Interface(network_contracts_1.StreamRegistryABI).getEvent('StreamCreated'),
                contractAddress: (0, utils_1.toEthereumAddress)(this.config.contracts.streamRegistryChainAddress)
            },
            sourceEmitter: chainEventPoller,
            targetName: 'streamCreated',
            targetEmitter: eventEmitter,
            transformation: (streamId, metadata, blockNumber) => ({
                streamId: (0, utils_1.toStreamID)(streamId),
                metadata: (0, StreamMetadata_1.parseMetadata)(metadata),
                blockNumber
            }),
            loggerFactory
        });
        this.metadataCache = (0, Mapping_1.createCacheMap)({
            valueFactory: (streamId) => {
                return this.getStreamMetadata_nonCached(streamId);
            },
            ...config.cache
        });
        this.publisherCache = (0, Mapping_1.createCacheMap)({
            valueFactory: ([streamId, userId]) => {
                return this.isStreamPublisherOrSubscriber_nonCached(streamId, userId, permission_1.StreamPermission.PUBLISH);
            },
            ...config.cache
        });
        this.subscriberCache = (0, Mapping_1.createCacheMap)({
            valueFactory: ([streamId, userId]) => {
                return this.isStreamPublisherOrSubscriber_nonCached(streamId, userId, permission_1.StreamPermission.SUBSCRIBE);
            },
            ...config.cache
        });
        this.publicSubscribePermissionCache = (0, Mapping_1.createCacheMap)({
            valueFactory: (streamId) => {
                return this.hasPermission({
                    streamId,
                    public: true,
                    permission: permission_1.StreamPermission.SUBSCRIBE
                });
            },
            ...config.cache
        });
    }
    async connectToContract() {
        if (this.streamRegistryContract === undefined) {
            const chainSigner = await this.identity.getTransactionSigner(this.rpcProviderSource);
            this.streamRegistryContract = this.contractFactory.createWriteContract((0, utils_1.toEthereumAddress)(this.config.contracts.streamRegistryChainAddress), network_contracts_1.StreamRegistryABI, chainSigner, 'streamRegistry');
        }
    }
    async createStream(streamId, metadata) {
        const ethersOverrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        const domainAndPath = utils_1.StreamIDUtils.getDomainAndPath(streamId);
        if (domainAndPath === undefined) {
            throw new Error(`stream id "${streamId}" not valid`);
        }
        const [domain, path] = domainAndPath;
        await this.connectToContract();
        if ((0, utils_1.isENSName)(domain)) {
            /*
                The call to createStreamWithENS delegates the ENS ownership check, and therefore the
                call doesn't fail e.g. if the user doesn't own the ENS name. To see whether the stream
                creation succeeeds, we need to poll the chain for stream existence. If the polling timeouts, we don't
                know what the actual error was. (Most likely it has nothing to do with timeout
                -> we don't use the error from until(), but throw an explicit error instead.)
            */
            await (0, contract_1.waitForTx)(this.streamRegistryContract.createStreamWithENS(domain, path, JSON.stringify(metadata), ethersOverrides));
            try {
                await (0, utils_1.until)(async () => this.streamExistsOnChain(streamId), 
                // eslint-disable-next-line no-underscore-dangle
                this.config._timeouts.ensStreamCreation.timeout, 
                // eslint-disable-next-line no-underscore-dangle
                this.config._timeouts.ensStreamCreation.retryInterval);
            }
            catch {
                throw new Error(`unable to create stream "${streamId}"`);
            }
        }
        else {
            await this.ensureStreamIdInNamespaceOfAuthenticatedUser(domain, streamId);
            await (0, contract_1.waitForTx)(this.streamRegistryContract.createStream(path, JSON.stringify(metadata), ethersOverrides));
        }
        this.populateMetadataCache(streamId, metadata);
    }
    async ensureStreamIdInNamespaceOfAuthenticatedUser(address, streamId) {
        const userAddress = (0, utils_1.toEthereumAddress)(await this.identity.getUserId());
        if (address !== userAddress) {
            throw new Error(`stream id "${streamId}" not in namespace of authenticated user "${userAddress}"`);
        }
    }
    async setStreamMetadata(streamId, metadata) {
        await this.connectToContract();
        const ethersOverrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        await (0, contract_1.waitForTx)(this.streamRegistryContract.updateStreamMetadata(streamId, JSON.stringify(metadata), ethersOverrides));
        this.populateMetadataCache(streamId, metadata);
    }
    async deleteStream(streamIdOrPath) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        await this.connectToContract();
        const ethersOverrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        await (0, contract_1.waitForTx)(this.streamRegistryContract.deleteStream(streamId, ethersOverrides));
        invalidateCache(this.metadataCache, streamId);
        this.invalidatePermissionCaches(streamId);
    }
    async streamExistsOnChain(streamIdOrPath) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        this.logger.debug('Check if stream exists on chain', { streamId });
        return this.streamRegistryContractReadonly.exists(streamId);
    }
    async getStreamMetadata_nonCached(streamId) {
        let metadata;
        try {
            metadata = await this.streamRegistryContractReadonly.getStreamMetadata(streamId);
        }
        catch (err) {
            return streamContractErrorProcessor(err, streamId, 'StreamRegistry');
        }
        return (0, StreamMetadata_1.parseMetadata)(metadata);
    }
    async *searchStreams(term, permissionFilter) {
        const queryResult = (0, searchStreams_1.searchStreams)(term, permissionFilter, this.theGraphClient);
        for await (const item of queryResult) {
            const id = (0, utils_1.toStreamID)(item.stream.id);
            this.populateMetadataCache(id, (0, StreamMetadata_1.parseMetadata)(item.stream.metadata));
            yield id;
        }
    }
    getStreamPublishers(streamIdOrPath) {
        return this.getStreamPublishersOrSubscribersList(streamIdOrPath, 'publishExpiration');
    }
    getStreamSubscribers(streamIdOrPath) {
        return this.getStreamPublishersOrSubscribersList(streamIdOrPath, 'subscribeExpiration');
    }
    async *getStreamPublishersOrSubscribersList(streamIdOrPath, fieldName) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        const backendResults = this.theGraphClient.queryEntities((lastId, pageSize) => StreamRegistry_1.buildStreamPublishersOrSubscribersQuery(streamId, fieldName, lastId, pageSize));
        /*
         * There can be orphaned permission entities if a stream is deleted (currently
         * we don't remove the assigned permissions, see ETH-222)
         * TODO remove the filtering when ETH-222 has been implemented, and remove also
         * stream result field in buildStreamPublishersOrSubscribersQuery as it is
         * no longer needed
         */
        const validItems = (0, GeneratorUtils_1.filter)(backendResults, (p) => p.stream !== null);
        yield* (0, GeneratorUtils_1.map)(validItems, (item) => (0, utils_1.toUserId)(item.userId));
    }
    static buildStreamPublishersOrSubscribersQuery(streamId, fieldName, lastId, pageSize) {
        const query = `
        {
            streamPermissions (
                first: ${pageSize}
                orderBy: "id"
                where: {
                    id_gt: "${lastId}"
                    stream: "${streamId}"
                    ${fieldName}_gt: "${Math.round(Date.now() / 1000)}"
                }
            ) {
                id
                userId
                stream {
                    id
                }
            }
        }`;
        return { query };
    }
    // --------------------------------------------------------------------------------------------
    // Permissions
    // --------------------------------------------------------------------------------------------
    async hasPermission(query) {
        if ((0, permission_1.isPublicPermissionQuery)(query)) {
            const permissionType = (0, permission_1.streamPermissionToSolidityType)(query.permission);
            return this.streamRegistryContractReadonly.hasPublicPermission(query.streamId, permissionType);
        }
        else {
            const chainPermissions = query.allowPublic
                ? await this.streamRegistryContractReadonly.getPermissionsForUserId(query.streamId, query.userId)
                : await this.streamRegistryContractReadonly.getDirectPermissionsForUserId(query.streamId, query.userId);
            const permissions = (0, permission_1.convertChainPermissionsToStreamPermissions)(chainPermissions);
            return permissions.includes(query.permission);
        }
    }
    async getPermissions(streamIdOrPath) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        const queryResults = await (0, utils_1.collect)(this.theGraphClient.queryEntities((lastId, pageSize) => {
            const query = `{
                    stream (id: "${streamId}") {
                        id
                        metadata
                        permissions(first: ${pageSize} orderBy: "id" where: { id_gt: "${lastId}"}) {
                            id
                            userId
                            canEdit
                            canDelete
                            publishExpiration
                            subscribeExpiration
                            canGrant
                        }
                    }
                }`;
            return { query };
        }, (response) => {
            if (response.stream !== null) {
                return response.stream.permissions;
            }
            else {
                throw new StreamrClientError_1.StreamrClientError('Stream not found: id=' + streamId, 'STREAM_NOT_FOUND');
            }
        }));
        const assignments = [];
        queryResults.forEach((permissionResult) => {
            const permissions = (0, permission_1.convertChainPermissionsToStreamPermissions)(permissionResult);
            /*
            * There can be query results, which don't contain any permissions. That happens if a
            * user revokes all permissions from a stream. Currently we don't remove these empty assignments
            * from The Graph index. TODO remove the "permission.length > 0" if/when we implement the
            * empty assignments cleanup in The Graph.
            */
            if (permissions.length > 0) {
                if (permissionResult.userId === permission_1.PUBLIC_PERMISSION_USER_ID) {
                    assignments.push({
                        public: true,
                        permissions
                    });
                }
                else {
                    assignments.push({
                        userId: (0, utils_1.toUserId)(permissionResult.userId),
                        permissions
                    });
                }
            }
        });
        return assignments;
    }
    async grantPermissions(streamIdOrPath, ...assignments) {
        validatePermissionAssignments(assignments);
        const overrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        return this.updatePermissions(streamIdOrPath, (streamId, userId, solidityType) => {
            return (userId === undefined)
                ? this.streamRegistryContract.grantPublicPermission(streamId, solidityType, overrides)
                : this.streamRegistryContract.grantPermissionForUserId(streamId, userId, solidityType, overrides);
        }, ...assignments);
    }
    async revokePermissions(streamIdOrPath, ...assignments) {
        validatePermissionAssignments(assignments);
        const overrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        return this.updatePermissions(streamIdOrPath, (streamId, userId, solidityType) => {
            return (userId === undefined)
                ? this.streamRegistryContract.revokePublicPermission(streamId, solidityType, overrides)
                : this.streamRegistryContract.revokePermissionForUserId(streamId, userId, solidityType, overrides);
        }, ...assignments);
    }
    async updatePermissions(streamIdOrPath, createTransaction, ...assignments) {
        const streamId = await this.streamIdBuilder.toStreamID(streamIdOrPath);
        this.invalidatePermissionCaches(streamId);
        await this.connectToContract();
        for (const assignment of assignments) {
            for (const permission of assignment.permissions) {
                const solidityType = (0, permission_1.streamPermissionToSolidityType)(permission);
                const userId = (0, permission_1.isPublicPermissionAssignment)(assignment) ? undefined : assignment.userId;
                const txToSubmit = createTransaction(streamId, userId, solidityType);
                await (0, contract_1.waitForTx)(txToSubmit);
            }
        }
    }
    async setPermissions(...items) {
        const streamIds = [];
        const targets = [];
        const chainPermissions = [];
        for (const item of items) {
            validatePermissionAssignments(item.assignments);
            const streamId = await this.streamIdBuilder.toStreamID(item.streamId);
            this.invalidatePermissionCaches(streamId);
            streamIds.push(streamId);
            targets.push(item.assignments.map((assignment) => {
                return (0, permission_1.isPublicPermissionAssignment)(assignment) ? permission_1.PUBLIC_PERMISSION_USER_ID : assignment.userId;
            }));
            chainPermissions.push(item.assignments.map((assignment) => {
                return (0, permission_1.convertStreamPermissionsToChainPermission)(assignment.permissions);
            }));
        }
        await this.connectToContract();
        const ethersOverrides = await (0, ethereumUtils_1.getEthersOverrides)(this.rpcProviderSource, this.config);
        const txToSubmit = this.streamRegistryContract.setMultipleStreamPermissionsForUserIds(streamIds, targets, chainPermissions, ethersOverrides);
        await (0, contract_1.waitForTx)(txToSubmit);
    }
    async isStreamPublisherOrSubscriber_nonCached(streamId, userId, permission) {
        try {
            return await this.hasPermission({ streamId, userId, permission, allowPublic: true });
        }
        catch (err) {
            return streamContractErrorProcessor(err, streamId, 'StreamPermission');
        }
    }
    // --------------------------------------------------------------------------------------------
    // Caching
    // --------------------------------------------------------------------------------------------
    getStreamMetadata(streamId) {
        return this.metadataCache.get(streamId);
    }
    isStreamPublisher(streamId, userId) {
        return this.publisherCache.get([streamId, userId]);
    }
    isStreamSubscriber(streamId, userId) {
        return this.subscriberCache.get([streamId, userId]);
    }
    hasPublicSubscribePermission(streamId) {
        return this.publicSubscribePermissionCache.get(streamId);
    }
    populateMetadataCache(streamId, metadata) {
        this.metadataCache.set(streamId, metadata);
    }
    invalidatePermissionCaches(streamId) {
        this.logger.trace('Clear permission caches for stream', { streamId });
        invalidateCache(this.publisherCache, streamId);
        invalidateCache(this.subscriberCache, streamId);
        // TODO should also clear cache for hasPublicSubscribePermission?
    }
};
exports.StreamRegistry = StreamRegistry;
exports.StreamRegistry = StreamRegistry = StreamRegistry_1 = __decorate([
    (0, tsyringe_1.scoped)(tsyringe_1.Lifecycle.ContainerScoped),
    __param(5, (0, tsyringe_1.inject)(Config_1.ConfigInjectionToken)),
    __param(6, (0, tsyringe_1.inject)(Identity_1.IdentityInjectionToken)),
    __metadata("design:paramtypes", [ContractFactory_1.ContractFactory,
        RpcProviderSource_1.RpcProviderSource,
        ChainEventPoller_1.ChainEventPoller,
        utils_1.TheGraphClient,
        StreamIDBuilder_1.StreamIDBuilder, Object, Identity_1.Identity,
        events_1.StreamrClientEventEmitter,
        LoggerFactory_1.LoggerFactory])
], StreamRegistry);
//# sourceMappingURL=StreamRegistry.js.map