"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const bson_1 = require("bson");
const chai_1 = require("chai");
const sinon = __importStar(require("sinon"));
const stream_1 = require("stream");
const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
const cache_1 = require("../../../src/models/cache");
const wallet_1 = require("../../../src/models/wallet");
const walletAddress_1 = require("../../../src/models/walletAddress");
const csp_1 = require("../../../src/modules/ethereum/api/csp");
const block_1 = require("../../../src/providers/chain-state/evm/models/block");
const transaction_1 = require("../../../src/providers/chain-state/evm/models/transaction");
const erigonDbBlocks_1 = require("../../data/ETH/erigonDbBlocks");
const erigonDbTransactions_1 = require("../../data/ETH/erigonDbTransactions");
const integration_1 = require("../../helpers/integration");
describe('Ethereum API', function () {
    const chain = 'ETH';
    const network = 'regtest';
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const suite = this;
    this.timeout(30000);
    before(integration_1.intBeforeHelper);
    after(async () => (0, integration_1.intAfterHelper)(suite));
    it('should return undefined for garbage data', () => {
        const data = 'garbage';
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.be.undefined;
    });
    it('should be able to classify ERC20 data', () => {
        const data = '0x095ea7b300000000000000000000000052de8d3febd3a06d3c627f59d56e6892b80dcf1200000000000000000000000000000000000000000000000000000000000f4240';
        transaction_1.EVMTransactionStorage.abiDecode(data);
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.exist;
        (0, chai_1.expect)(decoded.type).to.eq('ERC20');
    });
    it('should be able to classify ERC721 data', () => {
        const data = '0xa22cb465000000000000000000000000efc70a1b18c432bdc64b596838b4d138f6bc6cad0000000000000000000000000000000000000000000000000000000000000001';
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.exist;
        (0, chai_1.expect)(decoded.type).to.eq('ERC721');
    });
    it('should be able to classify Invoice data', () => {
        const data = '0xb6b4af0500000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000033ec500800000000000000000000000000000000000000000000000000000016e00f7b3d3c72c929edaf203cfabf7a0513cb8cee277a84ec3fd56bcf3f396b6d665c8abe6c4432f916bacafc94982b45050513de2ee5544aa855d9b5b60e8c1c94e71ffca000000000000000000000000000000000000000000000000000000000000001cfd9150848849c7aff74939535afe5e56dcac5f2f553467ae0e9181d14c0e49c9799433220e288e282376b86aae1bc1d683af4708b38999d59b5d65ff29a85705000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.exist;
        (0, chai_1.expect)(decoded.type).to.eq('INVOICE');
    });
    it('should handle multiple decodes', () => {
        const data = '0x095ea7b300000000000000000000000052de8d3febd3a06d3c627f59d56e6892b80dcf1200000000000000000000000000000000000000000000000000000000000f4240';
        transaction_1.EVMTransactionStorage.abiDecode(data);
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.exist;
        (0, chai_1.expect)(decoded.type).to.eq('ERC20');
        const data2 = '0xa22cb465000000000000000000000000efc70a1b18c432bdc64b596838b4d138f6bc6cad0000000000000000000000000000000000000000000000000000000000000001';
        transaction_1.EVMTransactionStorage.abiDecode(data);
        const decoded2 = transaction_1.EVMTransactionStorage.abiDecode(data2);
        (0, chai_1.expect)(decoded2).to.exist;
        (0, chai_1.expect)(decoded2.type).to.eq('ERC721');
    });
    it('should not crash when called with almost correct data', () => {
        const data = '0xa9059cbb0000000000000000000000000797350000000000000000000000000000000000000000000005150ac4c39a6f3f0000';
        const decoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        (0, chai_1.expect)(decoded).to.be.undefined;
    });
    it('should be able to get the fees', async () => {
        const chain = 'ETH';
        const network = 'testnet';
        let target = 1;
        while (target <= 4) {
            const cacheKey = `getFee-${chain}-${network}-${target}`;
            const fee = await csp_1.ETH.getFee({ chain, network, target });
            (0, chai_1.expect)(fee).to.exist;
            const cached = await cache_1.CacheStorage.getGlobal(cacheKey);
            (0, chai_1.expect)(fee).to.deep.eq(cached);
            target++;
        }
    });
    it('should be able to get type 2 fees', async () => {
        const sandbox = sinon.createSandbox();
        const chain = 'ETH';
        const network = 'mainnet';
        const rpc = {
            estimateMaxPriorityFee: () => { return 2; },
            estimateFee: () => { return 4; }
        };
        let err;
        sandbox.stub(csp_1.ETH, 'getWeb3').resolves({ rpc });
        try {
            // max fee
            const cacheKey = `getFee-${chain}-${network}-2-type2`;
            const fee = await csp_1.ETH.getFee({ chain, network, target: 2, txType: 2 });
            (0, chai_1.expect)(fee).to.exist;
            (0, chai_1.expect)(fee.feerate).to.equal(4);
            const cached = await cache_1.CacheStorage.getGlobal(cacheKey);
            (0, chai_1.expect)(fee).to.deep.eq(cached);
            // priority fee
            const cacheKeyPriorityFee = `getFee-${chain}-${network}-priorityFee-15`;
            const priorityFee = await csp_1.ETH.getPriorityFee({ chain, network, percentile: 15 });
            (0, chai_1.expect)(priorityFee).to.exist;
            (0, chai_1.expect)(priorityFee.feerate).to.equal(2);
            const cachedPriorityFee = await cache_1.CacheStorage.getGlobal(cacheKeyPriorityFee);
            (0, chai_1.expect)(priorityFee).to.deep.eq(cachedPriorityFee);
        }
        catch (error) {
            err = error;
        }
        sandbox.restore();
        (0, chai_1.expect)(err).to.be.undefined;
    });
    it('should estimate fees by most recent transactions', async () => {
        const chain = 'ETH';
        const network = 'testnet';
        const txs = new Array(4000).fill({}).map(_ => {
            return {
                chain,
                network,
                blockHeight: 1,
                gasPrice: 10 * 1e9
            };
        });
        await cache_1.CacheStorage.collection.remove({});
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
        const estimates = await Promise.all([1, 2, 3, 4].map(target => csp_1.ETH.getFee({ chain, network, target })));
        for (const estimate of estimates) {
            (0, chai_1.expect)(estimate.feerate).to.be.gt(0);
            (0, chai_1.expect)(estimate.feerate).to.be.eq(10000000000);
        }
    });
    it('should return cached fee for a minute', async () => {
        const chain = 'ETH';
        const network = 'testnet';
        const txs = new Array(4000).fill({}).map(_ => {
            return {
                chain,
                network,
                blockHeight: 1,
                gasPrice: 10 * 1e9
            };
        });
        await cache_1.CacheStorage.collection.remove({});
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
        let estimates = await Promise.all([1, 2, 3, 4].map(target => csp_1.ETH.getFee({ chain, network, target })));
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        estimates = await Promise.all([1, 2, 3, 4].map(target => csp_1.ETH.getFee({ chain, network, target })));
        for (const estimate of estimates) {
            (0, chai_1.expect)(estimate.feerate).to.be.gt(0);
            (0, chai_1.expect)(estimate.feerate).to.be.eq(10000000000);
        }
    });
    it('should be able to get address token balance', async () => {
        const sandbox = sinon.createSandbox();
        const address = '0xb8fd14fb0e0848cb931c1e54a73486c4b968be3d';
        const token = {
            name: 'Test Token',
            decimals: 10,
            symbol: 'TST'
        };
        const tokenStub = {
            methods: {
                name: () => ({ call: sandbox.stub().resolves(token.name) }),
                decimals: () => ({ call: sandbox.stub().resolves(token.decimals) }),
                symbol: () => ({ call: sandbox.stub().resolves(token.symbol) }),
                balanceOf: () => ({ call: sandbox.stub().resolves(0) })
            }
        };
        const rpc = {
            web3: {
                eth: {
                    Contract: sandbox.stub().returns(tokenStub)
                }
            }
        };
        sandbox.stub(csp_1.ETH, 'getWeb3').resolves(rpc);
        const balance = await csp_1.ETH.getBalanceForAddress({ chain, network, address, args: { tokenAddress: address } });
        (0, chai_1.expect)(balance).to.deep.eq({ confirmed: 0, unconfirmed: 0, balance: 0 });
        sandbox.restore();
    });
    it('should be able to get address ETH balance', async () => {
        const address = '0xb8fd14fb0e0848cb931c1e54a73486c4b968be3d';
        const balance = await csp_1.ETH.getBalanceForAddress({ chain, network, address, args: {} });
        (0, chai_1.expect)(balance).to.deep.eq({ confirmed: 0, unconfirmed: 0, balance: 0 });
    });
    it('should stream ETH transactions for address', async () => {
        const address = '0xb8fd14fb0e0848cb931c1e54a73486c4b968be3d';
        const txCount = 100;
        const txs = new Array(txCount).fill({}).map(() => {
            return {
                chain,
                network,
                blockHeight: 1,
                gasPrice: 10 * 1e9,
                data: Buffer.from(''),
                from: address
            };
        });
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
        const res = new stream_1.Transform({
            transform: (data, _, cb) => cb(null, data)
        });
        res.type = () => res;
        const req = new stream_1.Transform({
            transform: (_data, _, cb) => cb(null)
        });
        await csp_1.ETH.streamAddressTransactions({ chain, network, address, res, req, args: {} });
        let counter = 0;
        await new Promise(r => {
            res
                .on('data', () => counter++)
                .on('end', r);
        });
        const commaCount = txCount - 1;
        const bracketCount = 2;
        const expected = txCount + commaCount + bracketCount;
        (0, chai_1.expect)(counter).to.eq(expected);
    });
    it('should stream ETH transactions for block', async () => {
        const txCount = 100;
        const txs = new Array(txCount).fill({}).map(() => {
            return {
                chain,
                network,
                blockHeight: 1,
                gasPrice: 10 * 1e9,
                data: Buffer.from('')
            };
        });
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
        const res = new stream_1.Transform({
            transform: (data, _, cb) => {
                cb(null, data);
            }
        });
        res.type = () => res;
        const req = new stream_1.Transform({
            transform: (_data, _, cb) => {
                cb(null);
            }
        });
        await csp_1.ETH.streamTransactions({ chain, network, res, req, args: { blockHeight: 1 } });
        let counter = 0;
        await new Promise(r => {
            res
                .on('data', () => {
                counter++;
            })
                .on('end', () => {
                r();
            });
        });
        const commaCount = txCount - 1;
        const bracketCount = 2;
        const expected = txCount + commaCount + bracketCount;
        (0, chai_1.expect)(counter).to.eq(expected);
    });
    it('should stream ETH transactions for blockHash', async () => {
        const txCount = 100;
        const txs = new Array(txCount).fill({}).map(() => {
            return {
                chain,
                network,
                blockHash: '12345',
                gasPrice: 10 * 1e9,
                data: Buffer.from('')
            };
        });
        await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
        const res = new stream_1.Transform({
            transform: (data, _, cb) => {
                cb(null, data);
            }
        });
        res.type = () => res;
        const req = new stream_1.Transform({
            transform: (_data, _, cb) => {
                cb(null);
            }
        });
        await csp_1.ETH.streamTransactions({ chain, network, res, req, args: { blockHash: '12345' } });
        let counter = 0;
        await new Promise(r => {
            res
                .on('data', () => {
                counter++;
            })
                .on('end', () => {
                r();
            });
        });
        const commaCount = txCount - 1;
        const bracketCount = 2;
        const expected = txCount + commaCount + bracketCount;
        (0, chai_1.expect)(counter).to.eq(expected);
    });
    describe('#streamWalletTransactions', () => {
        const sandbox = sinon.createSandbox();
        const chain = 'ETH';
        const network = 'mainnet';
        const address = '0x1Eee23160Db790ee48Fd39871A64b13e76Fc2C3C';
        let wallet = {
            chain,
            network,
            pubKey: '',
            name: 'this-name',
            singleAddress: false,
            path: 'm/0/0'
        };
        const web3 = new crypto_wallet_core_1.Web3();
        before(async () => {
            const res = await wallet_1.WalletStorage.collection.findOneAndUpdate({ name: wallet.name }, { $set: wallet }, { returnOriginal: false, upsert: true });
            wallet = res.value;
            await walletAddress_1.WalletAddressStorage.collection.updateOne({ network, address }, { $set: { chain, network, wallet: wallet._id, processed: true, address } }, { upsert: true });
            sandbox.stub(csp_1.ETH, 'getWeb3').resolves({ web3 });
        });
        afterEach(async () => {
            await block_1.EVMBlockStorage.collection.deleteMany({});
            await transaction_1.EVMTransactionStorage.collection.deleteMany({});
        });
        after(async () => {
            sandbox.restore();
        });
        it('should stream wallet\'s valid ETH transactions', async () => await streamWalletTransactionsTest(chain, network));
        it('should stream wallet\'s valid & invalid ETH transactions', async () => await streamWalletTransactionsTest(chain, network, true));
        it('should stream DEX wallet transactions with erigon trace blocks', async () => {
            await block_1.EVMBlockStorage.collection.insertMany(erigonDbBlocks_1.ErigonEthBlocks);
            await transaction_1.EVMTransactionStorage.collection.insertMany(erigonDbTransactions_1.ErigonEthTransactions);
            await streamDexWalletTransactions(chain, network, wallet, address, web3);
        });
    });
});
const streamWalletTransactionsTest = async (chain, network, includeInvalidTxs = false) => {
    const sandbox = sinon.createSandbox();
    // Constants
    const address = '0x7F17aF79AABC4A297A58D389ab5905fEd4Ec9502';
    const objectId = bson_1.ObjectId.createFromHexString('60f9abed0e32086bf9903bb5');
    const wallet = {
        _id: objectId,
        chain,
        network,
        name: 'Ganache',
        pubKey: '0x029ec2ebdebe6966259cf3c6f35c4f126b82fe072bf9d0e81dad375f1d6d2d9054',
        path: 'm/44\'/60\'/0\'/0/0',
        singleAddress: true
    };
    const txCount = 100;
    // Valid Transactions
    const txs = new Array(txCount).fill({}).map(() => {
        return {
            chain,
            network,
            blockHeight: 1,
            gasPrice: 10 * 1e9,
            data: Buffer.from(''),
            from: address
        };
    });
    // Invalid Transactions
    for (let i = 0; i < txCount; i++) {
        txs.push({
            ...txs[0],
            blockHeight: -3
        });
    }
    // Add wallet object ID to transactions
    for (const tx of txs) {
        tx.wallets = [objectId];
    }
    // Stubs
    sandbox.stub(csp_1.ETH, 'getWalletAddresses').resolves([{ address }]);
    sandbox.stub(csp_1.ETH, 'isP2p').returns(true);
    // Test
    await transaction_1.EVMTransactionStorage.collection.deleteMany({});
    await transaction_1.EVMTransactionStorage.collection.insertMany(txs);
    let counter = 0;
    const req = new stream_1.Writable({
        write: function (data, _, cb) {
            data && counter++;
            cb();
        }
    });
    const res = new stream_1.Writable({
        write: function (data, _, cb) {
            data && counter++;
            cb();
        }
    });
    res.type = () => res;
    const err = await new Promise(r => {
        res
            .on('error', r)
            .on('finish', r);
        csp_1.ETH.streamWalletTransactions({
            chain,
            network,
            wallet,
            req,
            res,
            args: {
                includeInvalidTxs
            }
        })
            .catch(e => r(e));
    });
    (0, chai_1.expect)(err).to.not.exist;
    (0, chai_1.expect)(counter).to.eq(includeInvalidTxs ? txCount * 2 : txCount);
    sandbox.restore();
};
const streamDexWalletTransactions = async (chain, network, wallet, address, web3) => {
    await csp_1.ETH.updateWallet({ chain, network, wallet, addresses: [address] });
    const res = new stream_1.Transform({
        transform: (data, _, cb) => {
            cb(null, data);
        }
    });
    res.type = () => res;
    const req = new stream_1.Transform({
        transform: (_data, _, cb) => {
            cb(null);
        }
    });
    csp_1.ETH.streamWalletTransactions({ chain, network, wallet, res, req, args: {} });
    let total = BigInt(0);
    let totalRejected = BigInt(0);
    let totalFee = BigInt(0);
    await new Promise((resolve, reject) => {
        res.on('data', (data) => {
            try {
                const doc = JSON.parse(data.toString());
                if (doc.error) {
                    totalRejected += BigInt(doc.satoshis);
                }
                else {
                    total += BigInt(doc.satoshis);
                }
                if (doc.category !== 'receive' || doc.initialFrom === address) {
                    totalFee += BigInt(doc.fee);
                }
            }
            catch (e) {
                reject(e);
            }
        });
        res.on('finish', () => {
            try {
                const totalETH = web3.utils.fromWei(total.toString(), 'ether');
                const totalRejectedETH = web3.utils.fromWei(totalRejected.toString(), 'ether');
                const totalFeeETH = web3.utils.fromWei(totalFee.toString(), 'ether');
                const balanceETH = web3.utils.fromWei((total - totalFee).toString(), 'ether');
                // Need to slice b/c we're using Number rounding instead of BigInt
                (0, chai_1.expect)(balanceETH.slice(0, -5)).to.equal('309.666283810972788445'.slice(0, -5));
                (0, chai_1.expect)(totalETH.slice(0, -6)).to.equal('309.833211546562');
                (0, chai_1.expect)(totalFeeETH).to.equal('0.16692773559');
                (0, chai_1.expect)(totalRejectedETH.slice(0, -4)).to.equal('-3.99999999192707');
                resolve(true);
            }
            catch (e) {
                reject(e);
            }
        });
    });
};
//# sourceMappingURL=csp.test.js.map