"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletAddressStorage = exports.WalletAddressModel = void 0;
const stream_1 = require("stream");
const utils_1 = require("../utils");
const base_1 = require("./base");
const coin_1 = require("./coin");
const transaction_1 = require("./transaction");
class WalletAddressModel extends base_1.BaseModel {
    constructor(storage) {
        super('walletaddresses', storage);
        this.allowedPaging = [];
    }
    onConnect() {
        this.collection.createIndex({ chain: 1, network: 1, address: 1, wallet: 1 }, { background: true, unique: true });
        this.collection.createIndex({ chain: 1, network: 1, wallet: 1, address: 1 }, { background: true, unique: true });
        this.collection.createIndex({ chain: 1, network: 1, address: 1, lastQueryTime: 1 }, { background: true, sparse: true });
    }
    _apiTransform(walletAddress, options) {
        const transform = { address: walletAddress.address };
        if (options && options.object) {
            return transform;
        }
        return JSON.stringify(transform);
    }
    async updateCoins(params) {
        const { wallet, addresses, opts } = params;
        const { chain, network } = wallet;
        const { reprocess } = opts || {};
        class AddressInputStream extends stream_1.Readable {
            constructor() {
                super({ objectMode: true });
                this.addressBatches = (0, utils_1.partition)(addresses, 1000);
                this.index = 0;
            }
            _read() {
                if (this.index < this.addressBatches.length) {
                    this.push(this.addressBatches[this.index]);
                    this.index++;
                }
                else {
                    this.push(null);
                }
            }
        }
        class FilterExistingAddressesStream extends stream_1.Transform {
            constructor() {
                super({ objectMode: true });
            }
            async _transform(addressBatch, _, callback) {
                try {
                    if (reprocess) {
                        this.push(addressBatch);
                    }
                    else {
                        const exists = (await exports.WalletAddressStorage.collection
                            .find({ chain, network, wallet: wallet._id, address: { $in: addressBatch } })
                            .project({ address: 1, processed: 1 })
                            .toArray())
                            .filter(walletAddress => walletAddress.processed)
                            .map(walletAddress => walletAddress.address);
                        this.push(addressBatch.filter(address => !exists.includes(address)));
                    }
                    callback();
                }
                catch (err) {
                    callback(err);
                }
            }
        }
        class AddNewAddressesStream extends stream_1.Transform {
            constructor() {
                super({ objectMode: true });
            }
            async _transform(addressBatch, _, callback) {
                if (!addressBatch.length) {
                    return callback();
                }
                try {
                    await exports.WalletAddressStorage.collection.bulkWrite(addressBatch.map(address => ({
                        insertOne: {
                            document: { chain, network, wallet: wallet._id, address, processed: false }
                        }
                    })), { ordered: false });
                }
                catch (err) {
                    // Ignore duplicate keys, they may be half processed
                    if (err.code !== 11000) {
                        return callback(err);
                    }
                }
                this.push(addressBatch);
                callback();
            }
        }
        class UpdateCoinsStream extends stream_1.Transform {
            constructor() {
                super({ objectMode: true });
            }
            async _transform(addressBatch, _, callback) {
                if (!addressBatch.length) {
                    return callback();
                }
                try {
                    await coin_1.CoinStorage.collection.bulkWrite(addressBatch.map(address => ({
                        updateMany: {
                            filter: { chain, network, address },
                            update: { $addToSet: { wallets: wallet._id } }
                        }
                    })), { ordered: false });
                    this.push(addressBatch);
                    callback();
                }
                catch (err) {
                    callback(err);
                }
            }
        }
        class UpdatedTxidsStream extends stream_1.Transform {
            constructor() {
                super({ objectMode: true });
                this.txids = {};
            }
            async _transform(addressBatch, _, callback) {
                if (!addressBatch.length) {
                    return callback();
                }
                const coinStream = coin_1.CoinStorage.collection
                    .find({ chain, network, address: { $in: addressBatch } })
                    .project({ mintTxid: 1, spentTxid: 1 });
                coinStream.on('data', (coin) => {
                    if (!this.txids[coin.mintTxid]) {
                        this.txids[coin.mintTxid] = true;
                        this.push({ txid: coin.mintTxid });
                    }
                    if (!this.txids[coin.spentTxid]) {
                        this.txids[coin.spentTxid] = true;
                        this.push({ txid: coin.spentTxid });
                    }
                });
                let errored = false;
                coinStream.on('error', err => {
                    errored = true;
                    coinStream.destroy(err);
                    callback(err);
                });
                coinStream.on('end', () => {
                    if (errored) {
                        return;
                    }
                    this.push({ addressBatch });
                    callback();
                });
            }
        }
        class TxUpdaterStream extends stream_1.Transform {
            constructor() {
                super({ objectMode: true });
            }
            async _transform(data, _, callback) {
                const { txid, addressBatch } = data;
                if (addressBatch) {
                    this.push(addressBatch);
                    return callback();
                }
                try {
                    await transaction_1.TransactionStorage.collection.updateMany({ chain, network, txid }, { $addToSet: { wallets: wallet._id } });
                    callback();
                }
                catch (err) {
                    callback(err);
                }
            }
        }
        class MarkProcessedStream extends stream_1.Writable {
            constructor() {
                super({ objectMode: true });
            }
            async _write(addressBatch, _, callback) {
                if (!addressBatch.length) {
                    return callback();
                }
                try {
                    await exports.WalletAddressStorage.collection.bulkWrite(addressBatch.map(address => {
                        return {
                            updateOne: {
                                filter: { chain, network, address, wallet: wallet._id },
                                update: { $set: { processed: true } }
                            }
                        };
                    }), { ordered: false });
                    callback();
                }
                catch (err) {
                    callback(err);
                }
            }
        }
        const addressInputStream = new AddressInputStream();
        const filterExistingAddressesStream = new FilterExistingAddressesStream();
        const addNewAddressesStream = new AddNewAddressesStream();
        const updateCoinsStream = new UpdateCoinsStream();
        const updatedTxidsStream = new UpdatedTxidsStream();
        const txUpdaterStream = new TxUpdaterStream();
        const markProcessedStream = new MarkProcessedStream();
        const handleStreamError = (stream, reject) => {
            stream.on('error', err => {
                stream.destroy();
                return reject(err);
            });
        };
        return new Promise((resolve, reject) => {
            markProcessedStream.on('unpipe', () => {
                return resolve();
            });
            handleStreamError(filterExistingAddressesStream, reject);
            handleStreamError(addNewAddressesStream, reject);
            handleStreamError(updateCoinsStream, reject);
            handleStreamError(updatedTxidsStream, reject);
            handleStreamError(txUpdaterStream, reject);
            handleStreamError(markProcessedStream, reject);
            addressInputStream
                .pipe(filterExistingAddressesStream)
                .pipe(addNewAddressesStream)
                .pipe(updateCoinsStream)
                .pipe(updatedTxidsStream)
                .pipe(txUpdaterStream)
                .pipe(markProcessedStream);
        });
    }
    async updateLastQueryTime(params) {
        const { chain, network, address } = params;
        return this.collection.updateOne({ chain, network, address }, { $set: { lastQueryTime: new Date() } });
    }
}
exports.WalletAddressModel = WalletAddressModel;
exports.WalletAddressStorage = new WalletAddressModel();
//# sourceMappingURL=walletAddress.js.map