"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const grpc_web_1 = require("@improbable-eng/grpc-web");
const cids_1 = __importDefault(require("cids"));
const threads_crypto_1 = require("@textile/threads-crypto");
const loglevel_1 = __importDefault(require("loglevel"));
const threads_core_1 = require("@textile/threads-core");
const context_1 = require("@textile/context");
const pb = __importStar(require("@textile/threads-net-grpc/threadsnet_pb"));
const threadsnet_pb_service_1 = require("@textile/threads-net-grpc/threadsnet_pb_service");
const threads_encoding_1 = require("@textile/threads-encoding");
const logger = loglevel_1.default.getLogger('network-client');
function getThreadKeys(opts) {
    const threadKeys = new pb.Keys();
    opts.threadKey && threadKeys.setThreadkey(opts.threadKey.toBytes());
    opts.logKey && threadKeys.setLogkey(threads_core_1.marshalKey(opts.logKey));
    return threadKeys;
}
function threadRecordFromProto(proto, key) {
    return __awaiter(this, void 0, void 0, function* () {
        const threadID = threads_core_1.ThreadID.fromBytes(Buffer.from(proto.threadid, 'base64'));
        const rawID = Buffer.from(proto.logid, 'base64');
        const logID = threads_core_1.LogID.fromBytes(rawID);
        const record = proto.record && (yield threads_encoding_1.recordFromProto(proto.record, key.service));
        const info = {
            record,
            threadID,
            logID,
        };
        return info;
    });
}
function threadInfoFromProto(proto) {
    return __awaiter(this, void 0, void 0, function* () {
        const id = threads_core_1.ThreadID.fromBytes(Buffer.from(proto.threadid, 'base64'));
        const threadKey = Buffer.from(proto.threadkey, 'base64');
        const key = threads_core_1.ThreadKey.fromBytes(threadKey);
        const logs = new Set();
        for (const log of proto.logsList) {
            const rawId = Buffer.from(log.id, 'base64');
            const pid = threads_core_1.LogID.fromBytes(rawId);
            const logInfo = {
                id: pid,
                addrs: new Set(log.addrsList.map((addr) => new threads_core_1.Multiaddr(Buffer.from(addr, 'base64')))),
                head: log.head ? new cids_1.default(Buffer.from(log.head, 'base64')) : undefined,
                pubKey: threads_crypto_1.keys.unmarshalPublicKey(Buffer.from(log.pubkey, 'base64')),
            };
            if (log.privkey) {
                const pkBytes = Buffer.from(log.privkey, 'base64');
                logInfo.privKey = yield threads_crypto_1.keys.unmarshalPrivateKey(pkBytes);
            }
            logs.add(logInfo);
        }
        const addrs = new Set(proto.addrsList.map((addr) => new threads_core_1.Multiaddr(Buffer.from(addr, 'base64'))));
        return { id, key, logs, addrs };
    });
}
/**
 * Client is a web-gRPC wrapper client for communicating with a webgRPC-enabled Textile server.
 * This client library can be used to interact with a local or remote Threads gRPC Network.
 */
class Client {
    /**
     * Creates a new gRPC client instance for accessing the Textile Threads API.
     * @param context The context to use for interacting with the APIs. Can be modified later.
     */
    constructor(context = new context_1.Context('http://127.0.0.1:6007')) {
        this.context = context;
        this.serviceHost = context.host;
        this.rpcOptions = {
            transport: context.transport,
            debug: context.debug,
        };
        // If we have a default here, use it. Otherwise, rely on specific calls
        this.rpcOptions.transport && grpc_web_1.grpc.setDefaultTransport(this.rpcOptions.transport);
    }
    /**
     * Create a random user identity.
     */
    static randomIdentity() {
        return __awaiter(this, void 0, void 0, function* () {
            return threads_core_1.Libp2pCryptoIdentity.fromRandom();
        });
    }
    /**
     * Obtain a token for interacting with the remote API.
     * @param identity A user identity to use for creating records in the database. A random identity
     * can be created with `Client.randomIdentity(), however, it is not easy/possible to migrate
     * identities after the fact. Please store or otherwise persist any identity information if
     * you wish to retrieve user data later, or use an external identity provider.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    getToken(identity, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.getTokenChallenge(identity.public.toString(), (challenge) => __awaiter(this, void 0, void 0, function* () {
                return identity.sign(challenge);
            }), ctx);
        });
    }
    /**
     * Obtain a token for interacting with the remote API.
     * @param publicKey The public key of a user identity to use for creating records in the database.
     * A random identity can be created with `Client.randomIdentity(), however, it is not
     * easy/possible to migrate identities after the fact. Please store or otherwise persist any
     * identity information if you wish to retrieve user data later, or use an external identity
     * provider.
     * @param callback A callback function that takes a `challenge` argument and returns a signed
     * message using the input challenge and the private key associated with `publicKey`.
     * @param ctx Context object containing web-gRPC headers and settings.
     * @note `publicKey` must be the corresponding public key of the private key used in `callback`.
     */
    getTokenChallenge(publicKey, callback, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            const client = grpc_web_1.grpc.client(threadsnet_pb_service_1.API.GetToken, {
                host: this.serviceHost,
                transport: this.rpcOptions.transport,
                debug: this.rpcOptions.debug,
            });
            return new Promise((resolve, reject) => {
                let token = '';
                client.onMessage((message) => __awaiter(this, void 0, void 0, function* () {
                    if (message.hasChallenge()) {
                        const challenge = message.getChallenge_asU8();
                        const signature = yield callback(challenge);
                        const req = new pb.GetTokenRequest();
                        req.setSignature(signature);
                        client.send(req);
                        client.finishSend();
                    }
                    else if (message.hasToken()) {
                        token = message.getToken();
                    }
                }));
                client.onEnd((code, message, _trailers) => {
                    client.close();
                    if (code === grpc_web_1.grpc.Code.OK) {
                        this.context.withToken(token);
                        resolve(token);
                    }
                    else {
                        reject(new Error(message));
                    }
                });
                const req = new pb.GetTokenRequest();
                req.setKey(publicKey);
                const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
                client.start(metadata);
                client.send(req);
            });
        });
    }
    /**
     * getHostID returns the network's (remote) host peer ID.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    getHostID(ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making get host ID request');
            const req = new pb.GetHostIDRequest();
            const res = yield this.unary(threadsnet_pb_service_1.API.GetHostID, req, ctx);
            return threads_core_1.PeerId.BytesToString(res.getPeerid_asU8());
        });
    }
    /**
     * createThread with id.
     * @param id The Thread id.
     * @param opts The set of keys to use when creating the Thread. All keys are "optional", though if no replicator key
     * is provided, one will be created (and returned) on the remote network. Similarly, if no LogKey is provided, then
     * a private key will be generated (and returned) on the remote network. If no ReadKey is provided, the remote
     * network will be unable to write records (but it can return records).
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    createThread(id, opts, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making create thread request');
            const keys = getThreadKeys(opts);
            const req = new pb.CreateThreadRequest();
            req.setThreadid(id.toBytes());
            req.setKeys(keys);
            const res = yield this.unary(threadsnet_pb_service_1.API.CreateThread, req, ctx);
            return threadInfoFromProto(res.toObject());
        });
    }
    /**
     * addThread from a multiaddress.
     * @param addr The Thread multiaddr.
     * @param opts The set of keys to use when adding the Thread.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    addThread(addr, opts, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making add thread request');
            const keys = getThreadKeys(opts);
            const req = new pb.AddThreadRequest();
            req.setAddr(addr.buffer);
            req.setKeys(keys);
            const res = yield this.unary(threadsnet_pb_service_1.API.AddThread, req, ctx);
            return threadInfoFromProto(res.toObject());
        });
    }
    /**
     * getThread with id.
     * @param id The Thread ID.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    getThread(id, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making get thread request');
            const req = new pb.GetThreadRequest();
            req.setThreadid(id.toBytes());
            const res = yield this.unary(threadsnet_pb_service_1.API.GetThread, req, ctx);
            return threadInfoFromProto(res.toObject());
        });
    }
    /**
     * pullThread for new records.
     * @param id The Thread ID.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    pullThread(id, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making pull thread request');
            const req = new pb.PullThreadRequest();
            req.setThreadid(id.toBytes());
            yield this.unary(threadsnet_pb_service_1.API.PullThread, req, ctx);
            return;
        });
    }
    /**
     * deleteThread with id.
     * @param id The Thread ID.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    deleteThread(id, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making delete thread request');
            const req = new pb.DeleteThreadRequest();
            req.setThreadid(id.toBytes());
            yield this.unary(threadsnet_pb_service_1.API.DeleteThread, req, ctx);
            return;
        });
    }
    /**
     * addReplicator to a thread.
     * @param id The Thread ID.
     * @param addr The multiaddress of the replicator peer.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    addReplicator(id, addr, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making add replicator request');
            const req = new pb.AddReplicatorRequest();
            req.setThreadid(id.toBytes());
            req.setAddr(addr.buffer);
            const res = yield this.unary(threadsnet_pb_service_1.API.AddReplicator, req, ctx);
            return threads_core_1.PeerId.BytesToString(res.getPeerid_asU8());
        });
    }
    /**
     * createRecord with body.
     * @param id The Thread ID.
     * @param body The body to add as content.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    createRecord(id, body, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making create record request');
            const info = yield this.getThread(id, ctx);
            const block = threads_core_1.Block.encoder(body, 'dag-cbor').encode();
            const req = new pb.CreateRecordRequest();
            req.setThreadid(id.toBytes());
            req.setBody(block);
            const res = yield this.unary(threadsnet_pb_service_1.API.CreateRecord, req, ctx);
            return info.key && (yield threadRecordFromProto(res.toObject(), info.key));
        });
    }
    /**
     * addRecord to the given log.
     * @param id The Thread ID.
     * @param logID The Log ID.
     * @param rec The log record to add.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    addRecord(id, logID, rec, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making add record request');
            const prec = threads_encoding_1.recordToProto(rec);
            const req = new pb.AddRecordRequest();
            req.setThreadid(id.toBytes());
            req.setLogid(logID.toBytes());
            const record = new pb.Record();
            record.setBodynode(prec.bodynode);
            record.setEventnode(prec.eventnode);
            record.setHeadernode(prec.headernode);
            record.setRecordnode(prec.recordnode);
            req.setRecord(record);
            return this.unary(threadsnet_pb_service_1.API.AddRecord, req, ctx).then(() => undefined);
        });
    }
    /**
     * getRecord returns the record at cid.
     * @param id The Thread ID.
     * @param rec The record's CID.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    getRecord(id, rec, ctx) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.debug('making get record request');
            const info = yield this.getThread(id, ctx);
            if (info.key === undefined)
                throw new Error('Missing thread keys');
            const req = new pb.GetRecordRequest();
            req.setThreadid(id.toBytes());
            req.setRecordid(rec.buffer);
            const proto = yield this.unary(threadsnet_pb_service_1.API.GetRecord, req, ctx);
            const record = proto.toObject();
            if (!record.record)
                throw new Error('Missing return value');
            return threads_encoding_1.recordFromProto(record.record, info.key.service);
        });
    }
    /**
     * subscribe to new record events in the given threads.
     * @param cb The callback to call on each new thread record.
     * @param threads The variadic set of threads to subscribe to.
     * @param ctx Context object containing web-gRPC headers and settings.
     */
    subscribe(cb, threads = [], ctx) {
        logger.debug('making subscribe request');
        const ids = threads.map((thread) => thread.toBytes());
        const req = new pb.SubscribeRequest();
        req.setThreadidsList(ids);
        const keys = new Map(); // service key cache
        const client = grpc_web_1.grpc.client(threadsnet_pb_service_1.API.Subscribe, {
            host: this.serviceHost,
            transport: this.rpcOptions.transport,
            debug: this.rpcOptions.debug,
        });
        const get = (threadID, logID, message) => {
            const keyiv = keys.get(threadID);
            if (!keyiv)
                return cb(undefined, new Error('Missing service key'));
            const rec = message.getRecord();
            if (rec !== undefined) {
                threads_encoding_1.recordFromProto(rec.toObject(), keyiv).then((record) => {
                    return cb({ record, threadID, logID });
                });
            }
        };
        client.onMessage((message) => {
            if (message.hasRecord()) {
                const threadID = threads_core_1.ThreadID.fromBytes(message.getThreadid_asU8());
                const logID = threads_core_1.LogID.fromBytes(message.getLogid_asU8());
                if (!keys.has(threadID)) {
                    this.getThread(threadID, ctx).then((info) => {
                        var _a;
                        keys.set(threadID, (_a = info.key) === null || _a === void 0 ? void 0 : _a.service);
                        get(threadID, logID, message);
                    });
                }
                else {
                    get(threadID, logID, message);
                }
            }
            else {
                return cb(undefined, new Error('Missing record'));
            }
        });
        client.onEnd((code, message, _trailers) => {
            client.close();
            if (code !== grpc_web_1.grpc.Code.OK) {
                cb(undefined, new Error(message));
            }
            else {
                cb();
            }
        });
        const metadata = Object.assign(Object.assign({}, this.context.toJSON()), ctx === null || ctx === void 0 ? void 0 : ctx.toJSON());
        client.start(metadata);
        client.send(req);
        return { close: () => client.finishSend() };
    }
    unary(methodDescriptor, req, context) {
        return new Promise((resolve, reject) => {
            const creds = this.context.withContext(context);
            grpc_web_1.grpc.unary(methodDescriptor, {
                request: req,
                host: this.serviceHost,
                transport: this.rpcOptions.transport,
                debug: this.rpcOptions.debug,
                metadata: JSON.parse(JSON.stringify(creds)),
                onEnd: (res) => {
                    const { status, statusMessage, message } = res;
                    if (status === grpc_web_1.grpc.Code.OK) {
                        if (message) {
                            resolve(message);
                        }
                        else {
                            resolve();
                        }
                    }
                    else {
                        reject(new Error(statusMessage));
                    }
                },
            });
        });
    }
}
exports.Client = Client;
//# sourceMappingURL=index.js.map