"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.XRP = exports.RippleStateProvider = void 0;
const stream_1 = require("stream");
const util_1 = __importDefault(require("util"));
const crypto_rpc_1 = require("@bitpay-labs/crypto-rpc");
const request_1 = __importDefault(require("request"));
const config_1 = __importDefault(require("../../../config"));
const logger_1 = __importDefault(require("../../../logger"));
const cache_1 = require("../../../models/cache");
const walletAddress_1 = require("../../../models/walletAddress");
const internal_1 = require("../../../providers/chain-state/internal/internal");
const storage_1 = require("../../../services/storage");
const block_1 = require("../models/block");
const wallet_tx_transform_1 = require("./wallet-tx-transform");
class RippleStateProvider extends internal_1.InternalStateProvider {
    constructor(chain = 'XRP') {
        super(chain, wallet_tx_transform_1.RippleDbWalletTransactions);
        this.chain = chain;
        this.config = config_1.default.chains[this.chain];
    }
    async getClient(network) {
        if (!RippleStateProvider.clients[network]) {
            const networkConfig = this.config[network];
            const provider = networkConfig.provider;
            RippleStateProvider.clients[network] = new crypto_rpc_1.CryptoRpc({
                chain: this.chain,
                host: provider.host,
                port: provider.port,
                protocol: provider.protocol
            }).get(this.chain);
            await RippleStateProvider.clients[network].rpc.connect();
        }
        try {
            if (RippleStateProvider.clients[network].rpc.isConnected()) {
                await RippleStateProvider.clients[network].getBlock();
            }
            else {
                await RippleStateProvider.clients[network].rpc.connect();
            }
        }
        catch {
            await RippleStateProvider.clients[network].rpc.connect();
        }
        return RippleStateProvider.clients[network];
    }
    async getAccountNonce(network, address) {
        const client = await this.getClient(network);
        const info = await client.getAccountInfo({ address });
        return info?.account_data?.Sequence;
    }
    async getAccountFlags(network, address) {
        const client = await this.getClient(network);
        const info = await client.getAccountInfo({ address });
        return info?.account_flags;
    }
    async getWalletBalanceAtTime(params) {
        const { chain, network, time } = params;
        const addresses = await this.getWalletAddresses(params.wallet._id);
        const balances = await Promise.all(addresses.map(a => this.getBalanceForAddress({ address: a.address, chain, network, args: { time } })));
        return balances.reduce((total, current) => {
            total.balance += current.balance;
            total.confirmed += current.confirmed;
            total.unconfirmed += current.unconfirmed;
            return total;
        }, { confirmed: 0, unconfirmed: 0, balance: 0 });
    }
    async getBalanceForAddress(params) {
        const { chain, network, address, args } = params;
        const lowerAddress = address.toLowerCase();
        const cacheKey = `getBalanceForAddress-${chain}-${network}-${lowerAddress}`;
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            const client = await this.getClient(network);
            try {
                let ledgerIndex;
                if (args?.time) {
                    const block = await this.getBlockBeforeTime({ chain, network, time: args.time });
                    if (!block) {
                        throw new Error(`Balance not found at ${args.time}`);
                    }
                    ledgerIndex = block.height;
                }
                const balance = await client.getBalance({ address, ledgerIndex });
                const confirmed = Math.round(Number(balance) * 1e6);
                return { confirmed, unconfirmed: 0, balance: confirmed };
            }
            catch (e) {
                if (e?.data?.error_code === 19) {
                    // Error code for when we have derived an address,
                    // but the account has not yet been funded
                    return {
                        confirmed: 0,
                        unconfirmed: 0,
                        balance: 0
                    };
                }
                logger_1.default.error(`Error getting XRP balance for ${address} on ${network}: ${JSON.stringify(e.data) || e.stack || e.message || e}`);
                throw e;
            }
        }, args?.time ? cache_1.CacheStorage.Times.None : cache_1.CacheStorage.Times.Minute);
    }
    async getBlock(params) {
        const client = await this.getClient(params.network);
        const isHash = params.blockId && params.blockId.length == 64;
        const query = isHash ? { hash: params.blockId } : { index: Number(params.blockId) };
        const { ledger } = await client.getBlock(query);
        return this.transformLedger(ledger, params.network);
    }
    async getBlockBeforeTime(params) {
        const { chain, network, time = Date.now() } = params;
        const date = new Date(Math.min(new Date(time).getTime(), new Date().getTime())); // Date is at the most right now. This prevents excessive loop iterations below.
        if (date.toString() == 'Invalid Date') {
            throw new Error('Invalid time value');
        }
        const [block] = await block_1.XrpBlockStorage.collection
            .find({
            chain,
            network,
            timeNormalized: { $lte: date }
        })
            .limit(1)
            .sort({ timeNormalized: -1 })
            .toArray();
        if (!block) {
            return null;
        }
        let ledger = await this.getDataHostLedger(block.height, network);
        if (!ledger) {
            return null;
        }
        // Check if our DB has gaps. `block` might not be the latest block before `date`
        let workingIdx = Number(ledger.ledger_index) + 1; // +1 to check if the next block is < date
        ledger = await this.getDataHostLedger(workingIdx, network);
        while (ledger && new Date(ledger.close_time_human) < date) {
            // a gap exists
            workingIdx = Number(ledger.ledger_index) + 1;
            const timeGap = date.getTime() - new Date(ledger.close_time_human).getTime();
            if (timeGap > 1000 * 60 * 2) { // if more than a 2 min gap...
                workingIdx += Math.floor(timeGap / 10000); // ...jump forward assuming a block every 10 seconds
            }
            ledger = await this.getDataHostLedger(workingIdx, network);
        }
        // the timeGap above might have overshot
        while (!ledger || new Date(ledger.close_time_human) > date) {
            // walk it back
            workingIdx--;
            ledger = await this.getDataHostLedger(workingIdx, network);
        }
        return this.transformLedger(ledger, network);
    }
    async getDataHostLedger(index, network) {
        const ledger = await util_1.default.promisify(request_1.default.post).call(request_1.default, {
            url: this.config[network].provider.dataHost,
            json: true,
            body: {
                method: 'ledger',
                params: [
                    {
                        ledger_index: index,
                        transactions: true,
                        expand: false
                    }
                ]
            }
        });
        if (ledger?.body?.result?.status !== 'success') {
            return null;
        }
        return ledger.body.result.ledger;
    }
    async getFee(params) {
        const { chain, network, target } = params;
        const cacheKey = `getFee-${chain}-${network}-${target}`;
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            const client = await this.getClient(network);
            const fee = await client.estimateFee();
            return { feerate: parseFloat(fee), blocks: target };
        }, cache_1.CacheStorage.Times.Minute);
    }
    async broadcastTransaction(params) {
        const client = await this.getClient(params.network);
        const rawTxs = typeof params.rawTx === 'string' ? [params.rawTx] : params.rawTx;
        const txids = new Array();
        for (const tx of rawTxs) {
            const hash = (await client.sendRawTransaction({ rawTx: tx }));
            txids.push(hash);
        }
        return txids.length === 1 ? txids[0] : txids;
    }
    async getWalletBalance(params) {
        const { chain, network } = params;
        const addresses = await this.getWalletAddresses(params.wallet._id);
        const balances = await Promise.all(addresses.map(a => this.getBalanceForAddress({ address: a.address, chain, network, args: {} })));
        return balances.reduce((total, current) => {
            total.balance += current.balance;
            total.confirmed += current.confirmed;
            total.unconfirmed += current.unconfirmed;
            return total;
        }, { confirmed: 0, unconfirmed: 0, balance: 0 });
    }
    streamTxs(txs, stream) {
        for (const tx of txs) {
            stream.push(tx);
        }
    }
    async getAddressTransactions(params) {
        const { startTx, limitArg } = params.args;
        const client = await this.getClient(params.network);
        const serverInfo = await client.getServerInfo();
        const ledgers = serverInfo.complete_ledgers.split('-');
        const minLedgerIndex = Number(ledgers[0]);
        const allTxs = [];
        const limit = Number(limitArg) || 100;
        const options = {
            ledger_index_min: minLedgerIndex,
            limit,
            binary: false
        };
        if (startTx) {
            const tx = await client.getTransaction({ txid: startTx });
            options.ledger_index_min = Math.max(Number(tx?.ledger_index), minLedgerIndex);
            options.forward = true;
        }
        let txs = await client.getTransactions({ address: params.address, options });
        if (startTx) {
            const startTxIdx = txs.transactions.findIndex(tx => tx.tx?.hash === startTx);
            if (startTxIdx > -1) {
                txs.transactions = txs.transactions.slice(startTxIdx + 1);
            }
        }
        allTxs.push(...txs.transactions);
        while (txs.marker) {
            txs = await client.getTransactions({ address: params.address, options: {
                    marker: txs.marker,
                    limit,
                    binary: false
                } });
            allTxs.push(...txs.transactions);
        }
        return allTxs;
    }
    async streamAddressTransactions(params) {
        const readable = new stream_1.Readable({ objectMode: true });
        const txs = await this.getAddressTransactions(params);
        const transformed = txs.map(tx => this.transformAccountTx(tx, params.network));
        this.streamTxs(transformed, readable);
        readable.push(null);
        storage_1.Storage.stream(readable, params.req, params.res);
    }
    async streamTransactions(params) {
        const client = await this.getClient(params.network);
        const { blockHash } = params.args;
        const { ledger } = await client.getBlock({ hash: blockHash, transactions: true });
        const readable = new stream_1.Readable({ objectMode: true });
        const txs = ledger.transactions || [];
        this.streamTxs(txs, readable);
        readable.push(null);
        storage_1.Storage.stream(readable, params.req, params.res);
    }
    async getTransaction(params) {
        const client = await this.getClient(params.network);
        try {
            const tx = await client.getTransaction({ txid: params.txId });
            return tx;
        }
        catch {
            return undefined;
        }
    }
    async getLocalTip(params) {
        return block_1.XrpBlockStorage.getLocalTip(params);
    }
    async getCoinsForTx() {
        return {
            inputs: [],
            outputs: []
        };
    }
    transformLedger(ledger, network) {
        const txs = ledger.transactions || [];
        return {
            chain: this.chain,
            network,
            hash: ledger.ledger_hash,
            height: Number(ledger.ledger_index),
            previousBlockHash: ledger.parent_hash,
            processed: ledger.closed,
            time: new Date(ledger.close_time_human),
            timeNormalized: new Date(ledger.close_time_human),
            reward: 0,
            size: txs.length,
            transactionCount: txs.length,
            nextBlockHash: ''
        };
    }
    transform(tx, network, block) {
        const date = block?.time ? new Date(block?.time) : this.getDateFromRippleTime(tx.date) || '';
        const metaData = tx.metaData || tx.meta;
        const value = metaData.delivered_amount ??
            metaData.DeliveredAmount ??
            tx.Amount ??
            0;
        return {
            network,
            chain: this.chain,
            txid: tx.hash,
            from: tx.Account,
            blockHash: block?.hash || '',
            blockHeight: block?.height || tx.ledger_index,
            blockTime: date,
            blockTimeNormalized: date,
            value: Number(value),
            fee: Number(tx.Fee ?? 0),
            nonce: Number(tx.Sequence),
            destinationTag: tx.DestinationTag,
            to: tx.Destination,
            currency: undefined, // TODO
            invoiceID: tx.InvoiceID,
            wallets: []
        };
    }
    transformAccountTx(tx, network) {
        const date = this.getDateFromRippleTime(tx.tx?.date) || '';
        const value = Number(tx.meta.DeliveredAmount ?? tx.meta.delivered_amount ?? 0);
        return {
            network,
            chain: this.chain,
            txid: tx.tx?.hash,
            from: tx.tx?.Account,
            blockHash: tx.ledger_hash || '', // TODO: ledger_hash is not a property of AccountTransaction
            blockHeight: tx.tx?.ledger_index,
            blockTime: date,
            blockTimeNormalized: date,
            value: Number(value),
            fee: Number(tx.tx?.Fee ?? 0),
            nonce: Number(tx.tx?.Sequence),
            destinationTag: tx.tx.DestinationTag,
            to: tx.tx.Destination,
            currency: undefined, // TODO
            invoiceID: tx.tx.InvoiceID,
            wallets: []
        };
    }
    transformToCoins(tx, network, block) {
        const coins = [];
        const mintTxid = tx.hash || tx.tx?.hash || tx.hash;
        const metaData = tx.metaData || tx.meta || tx.meta;
        if (!metaData.AffectedNodes?.length) {
            return coins;
        }
        const nodes = metaData.AffectedNodes || [];
        for (const node of nodes) {
            if ('ModifiedNode' in node && node.ModifiedNode.FinalFields) {
                const address = node.ModifiedNode.FinalFields.Account;
                const value = Number(node.ModifiedNode.FinalFields.Balance) - Number(node.ModifiedNode.PreviousFields?.Balance);
                const coin = {
                    chain: this.chain,
                    network,
                    address,
                    value,
                    coinbase: false,
                    mintHeight: block?.height,
                    mintIndex: nodes.indexOf(node),
                    mintTxid,
                    wallets: []
                };
                coins.push(coin);
            }
            else if ('CreatedNode' in node) {
                const address = node.CreatedNode.NewFields.Account;
                const value = Number(node.CreatedNode.NewFields.Balance);
                const coin = {
                    chain: this.chain,
                    network,
                    address,
                    value,
                    coinbase: false,
                    mintHeight: block?.height,
                    mintIndex: nodes.indexOf(node),
                    mintTxid,
                    wallets: []
                };
                coins.push(coin);
            }
            else if ('DeletedNode' in node) {
                const address = node.DeletedNode.FinalFields.Account;
                const value = -1 * Number(node.DeletedNode.FinalFields.Balance);
                const coin = {
                    chain: this.chain,
                    network,
                    address,
                    value,
                    coinbase: false,
                    mintHeight: block?.height,
                    mintIndex: nodes.indexOf(node),
                    mintTxid,
                    wallets: []
                };
                coins.push(coin);
            }
        }
        return coins;
    }
    async tag(chain, network, tx, outputs) {
        const address = tx.from;
        const involvedAddress = [address];
        const transaction = { ...tx, wallets: new Array() };
        let coins = new Array();
        if (Array.isArray(outputs)) {
            coins = outputs.map(c => {
                return { ...c, wallets: new Array() };
            });
            involvedAddress.push(...coins.map(c => c.address));
        }
        const walletAddresses = await walletAddress_1.WalletAddressStorage.collection
            .find({ chain, network, address: { $in: involvedAddress } })
            .toArray();
        if ('chain' in tx) {
            transaction.wallets = walletAddresses.map(wa => wa.wallet);
        }
        if (coins && coins.length) {
            for (const coin of coins) {
                const coinWalletAddresses = walletAddresses.filter(wa => coin.address && wa.address === coin.address);
                if (coinWalletAddresses && coinWalletAddresses.length) {
                    coin.wallets = coinWalletAddresses.map(wa => wa.wallet);
                }
            }
        }
        return { transaction, coins };
    }
    async getReserve(network) {
        const client = await this.getClient(network);
        const info = await client.getServerInfo();
        return info.validated_ledger.reserve_base_xrp * 1e6;
    }
    getDateFromRippleTime(rippleTime) {
        if (rippleTime == null) {
            return null;
        }
        // the ripple epoch is 2000-01-01
        return new Date(new Date('2000-01-01').getTime() + rippleTime * 1000);
    }
}
exports.RippleStateProvider = RippleStateProvider;
RippleStateProvider.clients = {};
exports.XRP = new RippleStateProvider();
//# sourceMappingURL=csp.js.map