"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const rpc_core_1 = require("@polkadot/rpc-core");
const util_crypto_1 = require("@polkadot/util-crypto");
const CONSTS = __importStar(require("./consts"));
const defs_1 = __importDefault(require("./defs"));
const registry_1 = require("./registry");
const util_1 = require("./util");
let instanceCounter = 0;
const rxjs_1 = require("rxjs");
class ApiLite {
    #instanceId;
    isConnected;
    #connectedSub;
    #genesisHash;
    #runtimeSub;
    #runtimeVersion;
    // eslint-disable-next-line
    #keepaliveInterval;
    #noEnum;
    #feeSignedExtension;
    #feeAssetId;
    /** @deprecated */
    #other;
    _debug = false;
    registry = new registry_1.LiteRegistry();
    rpc;
    get genesisHash() {
        if (!this.#genesisHash) {
            throw new Error("not initialized");
        }
        return this.#genesisHash;
    }
    static create(provider, params) {
        const api = new ApiLite(provider, params ?? {});
        return api.isConnected.pipe((0, rxjs_1.filter)((v) => !!v), (0, rxjs_1.map)(() => api));
    }
    async #init() {
        const sub = this.rpc.chain.getBlockHash(0).subscribe((genesisHash) => {
            this.#genesisHash = genesisHash.toHex();
            sub.unsubscribe();
        });
        this.#keepaliveInterval = setInterval(() => {
            const sub = this.rpc.system.health().subscribe(() => {
                sub.unsubscribe();
            });
        }, 10000);
        this.#runtimeSub = (0, rxjs_1.combineLatest)([
            this.rpc.state.subscribeRuntimeVersion(),
        ]).subscribe(([runtimeVersion]) => {
            this.#runtimeVersion = runtimeVersion;
        });
    }
    async #destroy() {
        if (this.#keepaliveInterval) {
            clearInterval(this.#keepaliveInterval);
        }
        this.#connectedSub.unsubscribe();
        this.#runtimeSub?.unsubscribe();
    }
    constructor(provider, { skipRegistry = false, noEnum = false, feeSignedExtension = "ChargeTransactionPayment", feeAssetId, ...other } = {}) {
        this.#other = other;
        this.#feeSignedExtension = feeSignedExtension;
        this.#feeAssetId = feeAssetId;
        this.#noEnum = noEnum;
        let ready = false;
        this.#instanceId = `lite:${instanceCounter++}`;
        this.rpc = new rpc_core_1.RpcCore(this.#instanceId, this.registry, provider);
        this.isConnected = new rxjs_1.BehaviorSubject(this.rpc.provider.isConnected);
        this.rpc.provider.on("connected", () => this.isConnected.next(true));
        this.rpc.provider.on("disconnected", () => this.isConnected.next(false));
        this.#connectedSub = this.isConnected.subscribe(async (isConnected) => {
            if (isConnected) {
                if (!ready) {
                    await this.#init();
                }
                ready = true;
            }
            else if (ready) {
                await this.#destroy();
                ready = false;
            }
        });
        if (!skipRegistry) {
            this.registry.register(defs_1.default);
        }
    }
    #signingHeader() {
        return (0, rxjs_1.combineLatest)([
            this.rpc.chain
                .getHeader()
                .pipe((0, rxjs_1.switchMap)((header) => header.parentHash.isEmpty
                ? (0, rxjs_1.of)(header)
                : this.rpc.chain.getHeader(header.parentHash))),
            this.rpc.chain
                .getFinalizedHead()
                .pipe((0, rxjs_1.switchMap)((hash) => this.rpc.chain.getHeader(hash))),
        ]).pipe((0, rxjs_1.map)(([current, finalized]) => current.number.unwrap().toNumber() - finalized.number.toNumber() >
            CONSTS.MAX_FINALITY_LAG
            ? current
            : finalized));
    }
    #signingInfo(address, era) {
        const CURRENT = CONSTS.CHAIN.KUSAMA; // TODO multiple chain support
        return (0, rxjs_1.combineLatest)([
            this.rpc.system.accountNextIndex(address),
            !era || (typeof era === "number" && era > 0)
                ? this.#signingHeader()
                : (0, rxjs_1.of)(null),
        ]).pipe((0, rxjs_1.map)(([nonce, header]) => ({
            header,
            mortalLength: Math.min(CURRENT.SYSTEM_BLOCKHASHCOUNT ?? CONSTS.FALLBACK_MAX_HASH_COUNT, Math.floor(CONSTS.MORTAL_PERIOD /
                (CURRENT.BABE_EXPECTEDBLOCKTIME ??
                    CURRENT.TIMESTAMP_MINIMUMPERIOD / 2 ??
                    CONSTS.FALLBACK_PERIOD) +
                CONSTS.MAX_FINALITY_LAG)),
            nonce,
        })));
    }
    #serializeExtra = (nonce, tip = 0) => {
        const baseExtra = this.registry
            .createType("[Compact<u32>; 2]", [nonce, tip])
            .toHex()
            .slice(2);
        if (this.#feeSignedExtension === "ChargeTransactionPayment") {
            return baseExtra;
        }
        const extra = baseExtra +
            (this.#feeAssetId
                ? "01" + // feeAssetId is Option<u32>
                    this.registry
                        .createType("u32", this.#feeAssetId)
                        .toHex(true)
                        .slice(2)
                : "00");
        console.log({ extra });
        return extra;
    };
    #queryInfo = (extrinsic) => this.#other?.noRuntimeCalls
        ? this.rpc.payment.queryInfo(extrinsic)
        : this.rpc.state
            .call("TransactionPaymentApi_query_info", `${extrinsic}00000000`)
            .pipe((0, rxjs_1.map)((response) => {
            const hex = response.toHex();
            try {
                return this.registry.createType("RuntimeDispatchInfo", hex);
            }
            catch (err) {
                console.error(err);
                return undefined;
            }
        }));
    account(addresses) {
        const endpoints = addresses.map((address) => "0x" +
            CONSTS.STORAGE_KEYS.PALLETS.SYSTEM +
            CONSTS.STORAGE_KEYS.METHODS.ACCOUNT +
            (0, util_1.blake2Concat)((0, util_crypto_1.decodeAddress)(address)).slice(2));
        return this.rpc.state.subscribeStorage(endpoints).pipe((0, rxjs_1.map)((v) => v.map((info) => {
            const hex = info.unwrapOr(undefined)?.toHex();
            if (hex) {
                return this.registry.createType("AccountInfo", hex);
            }
            return undefined;
        })));
    }
    /** @deprecated for now; need to parse events w/o metadata */
    eventsAt(blockHash) {
        const endpoint = "0x" +
            CONSTS.STORAGE_KEYS.PALLETS.SYSTEM +
            CONSTS.STORAGE_KEYS.METHODS.EVENTS;
        return this.rpc.state.queryStorageAt([endpoint], blockHash);
    }
    pubByAddress(address) {
        return `0x${Buffer.from((0, util_crypto_1.decodeAddress)(address)).toString("hex")}`;
    }
    #signAndSend(signer, address, { era /** TODO tip */ }, method) {
        const pub = this.pubByAddress(address);
        const status$ = new rxjs_1.BehaviorSubject(undefined);
        return this.#signingInfo(address, era).pipe((0, rxjs_1.switchMap)((info) => {
            const payload = {
                address,
                blockHash: info.header?.hash.toHex() ?? this.#genesisHash ?? "0x0",
                blockNumber: info.header?.number.toHex() ?? "0x00000000",
                era: this.registry
                    .createTypeUnsafe("ExtrinsicEra", [
                    {
                        current: info.header?.number ?? 0,
                        period: era ?? info.mortalLength,
                    },
                ])
                    .toHex(),
                genesisHash: this.#genesisHash ?? "0x0",
                method,
                nonce: info.nonce.toHex(),
                signedExtensions: [
                    "CheckNonZeroSender",
                    "CheckSpecVersion",
                    "CheckTxVersion",
                    "CheckGenesis",
                    "CheckMortality",
                    "CheckNonce",
                    "CheckWeight",
                    this.#feeSignedExtension,
                ].filter((extension) => {
                    if (this.#other?.noCheckZeroSpenderExrension &&
                        extension === "CheckNonZeroSender") {
                        return false;
                    }
                    return true;
                }),
                specVersion: this.#runtimeVersion?.specVersion.toHex() ?? "0x0",
                tip: "0x00000000000000000000000000000000",
                transactionVersion: this.#runtimeVersion?.transactionVersion?.toHex() ?? "0x0",
                version: 4,
            };
            if (this.#feeSignedExtension === "ChargeAssetTxPayment") {
                // @ts-ignore
                payload.assetId = this.#feeAssetId || null;
            }
            return (0, rxjs_1.of)(payload).pipe((0, rxjs_1.switchMap)((p) => signer.signPayload?.(p) ?? (0, rxjs_1.of)(undefined)), (0, rxjs_1.switchMap)((result) => {
                if (!result) {
                    return (0, rxjs_1.of)(undefined);
                }
                const prefix = `${CONSTS.MAGIC_VAR}${this.#noEnum ? "" : "00"}`;
                const extra = this.#serializeExtra(info.nonce.toNumber());
                const msg = `${prefix}${pub.slice(2)}${result.signature.slice(2)}${payload.era.slice(2)}${extra}${method.slice(2)}`;
                const len = msg.length / 2;
                const message = this.registry.createType("[Compact<u32>; 1]", [len]).toHex() +
                    msg;
                if (this._debug) {
                    console.log({ extrinsic: message });
                }
                return this.rpc.provider.subscribe("author_extrinsicUpdate", "author_submitAndWatchExtrinsic", [message], (err, raw) => {
                    const status = this.registry.createType("ExtrinsicStatus", raw);
                    if (!err) {
                        status$.next(status);
                    }
                    else {
                        console.error(err);
                        // TODO proper error handling
                    }
                });
            }));
        }), (0, rxjs_1.combineLatestWith)(status$), (0, rxjs_1.tap)(([subId, status]) => {
            if (subId &&
                (status?.isRetracted ||
                    status?.isFinalityTimeout ||
                    status?.isFinalized ||
                    status?.isUsurped ||
                    status?.isDropped ||
                    status?.isInvalid)) {
                this.rpc.provider.unsubscribe("author_extrinsicUpdate", "author_submitAndWatchExtrinsic", subId);
            }
        }), (0, rxjs_1.map)(([, status]) => {
            return status;
        }));
    }
    #getSigMessage(address, method) {
        const pub = this.pubByAddress(address);
        return this.#signingInfo(address, undefined).pipe((0, rxjs_1.map)((info) => {
            const prefix = `${CONSTS.MAGIC_VAR}${this.#noEnum ? "" : "00"}`;
            const era = this.registry
                .createTypeUnsafe("ExtrinsicEra", [
                {
                    current: info.header?.number ?? 0,
                    period: info.mortalLength,
                },
            ])
                .toHex();
            const extra = this.#serializeExtra(info.nonce.toNumber());
            const msg = `${prefix}${pub.slice(2)}${CONSTS.FAKE_SIGNATURE.slice(2)}${era.slice(2)}${extra}${method.slice(2)}`;
            const len = msg.length / 2;
            const extrinsic = this.registry.createType("[Compact<u32>; 1]", [len]).toHex() + msg;
            if (this._debug) {
                console.log({ extrinsic });
            }
            return extrinsic;
        }));
    }
    createTx = (callIndex, ...args) => {
        const method = `0x${callIndex}${args.join("")}`;
        if (this._debug) {
            console.log({ method });
        }
        return {
            _api: this,
            _getFakeSigMessage: (address) => this.#getSigMessage(address, method),
            _method: method,
            paymentInfo: (address) => this.#getSigMessage(address, method).pipe((0, rxjs_1.switchMap)((message) => {
                // @ts-expect-error
                return this.#queryInfo(message);
            })),
            signAndSend: (address, signer, { era /** TODO tip */ }) => this.#signAndSend(signer, address, { era }, method),
            method,
        };
    };
    batchAll(methods) {
        const callIndex = "1a02";
        const formattedMethods = methods.map((method) => method.slice(2)).join("");
        const callsLength = this.registry
            .createType("[Compact<u32>; 1]", [methods.length])
            .toHex()
            .slice(2);
        const method = `0x${callIndex}${callsLength}${formattedMethods}`;
        return {
            _getFakeSigMessage: (address) => this.#getSigMessage(address, method),
            paymentInfo: (address) => this.#getSigMessage(address, method).pipe((0, rxjs_1.switchMap)((message) => {
                return this.rpc.payment.queryInfo(message);
            })),
            signAndSend: (address, signer, { era /** TODO tip */ }) => this.#signAndSend(signer, address, { era }, method),
            method,
        };
    }
    systemRemark(data) {
        const callIndex = "0001";
        const remark = Buffer.from(data).toString("hex");
        const remarkLength = this.registry
            .createType("[Compact<u32>; 1]", [data.length])
            .toHex()
            .slice(2);
        const method = `0x${callIndex}${remarkLength}${remark}`;
        return {
            _getFakeSigMessage: (address) => this.#getSigMessage(address, method),
            paymentInfo: (address) => this.#getSigMessage(address, method).pipe((0, rxjs_1.switchMap)((message) => {
                return this.rpc.payment.queryInfo(message);
            })),
            signAndSend: (address, signer, { era /** TODO tip */ }) => this.#signAndSend(signer, address, { era }, method),
            method,
        };
    }
    balanceTransfer(pub, amount) {
        const callIndex = "0500";
        const address = (0, util_crypto_1.encodeAddress)(pub /** TODO get ss58 from chain */);
        const dest = this.registry
            .createType("MultiAddress", { Id: pub })
            .toHex()
            .slice(2);
        const balance = this.registry
            .createType("[Compact<u128>; 1]", [amount])
            .toHex()
            .slice(2);
        const method = `0x${callIndex}${dest}${balance}`;
        return {
            _getFakeSigMessage: () => this.#getSigMessage(address, method),
            paymentInfo: () => this.#getSigMessage(address, method).pipe((0, rxjs_1.switchMap)((message) => {
                return this.rpc.payment.queryInfo(message);
            })),
            signAndSend: (signer, { era /** TODO tip */ }) => this.#signAndSend(signer, address, { era }, method),
            method,
        };
    }
    xcmTransfer(pub, amount) {
        const callIndex = "6302";
        const dest = this.registry
            .createType("XcmVersionedMultiLocation", {
            V1: {
                parents: 0,
                interior: {
                    X1: {
                        Parachain: 2024, // TODO no hardcode
                    },
                },
            },
        })
            .toHex()
            .slice(2);
        const beneficiary = this.registry
            .createType("XcmVersionedMultiLocation", {
            V1: {
                parents: 0,
                interior: {
                    X1: {
                        AccountId32: {
                            network: "Kusama",
                            id: pub,
                        },
                    },
                },
            },
        })
            .toHex()
            .slice(2);
        const assets = this.registry
            .createType("XcmVersionedMultiAssets", {
            V1: [
                {
                    id: {
                        Concrete: {
                            parents: 0,
                            interior: "Here",
                        },
                    },
                    fun: {
                        Fungible: amount,
                    },
                },
            ],
        })
            .toHex()
            .slice(2);
        const feeAssetItem = this.registry.createType("u32", 0).toHex().slice(2);
        const method = `0x${callIndex}${dest}${beneficiary}${assets}${feeAssetItem}`;
        const address = (0, util_crypto_1.encodeAddress)(pub /** TODO get ss58 from chain */);
        const getFakeSigMessage = () => this.#signingInfo(address, undefined).pipe((0, rxjs_1.map)((info) => {
            const prefix = `${CONSTS.MAGIC_VAR}${this.#noEnum ? "" : "00"}`;
            const msg = `${prefix}${pub.slice(2)}${CONSTS.FAKE_SIGNATURE.slice(2)}00${this.registry
                .createType("[Compact<u32>; 2]", [
                info.nonce.toNumber(),
                0 /* TODO tip */,
            ])
                .toHex()
                .slice(2)}${method.slice(2)}`;
            const len = msg.length / 2;
            return (this.registry.createType("[Compact<u32>; 1]", [len]).toHex() + msg);
        }));
        return {
            _getFakeSigMessage: getFakeSigMessage,
            paymentInfo: () => getFakeSigMessage().pipe((0, rxjs_1.switchMap)((message) => {
                return this.rpc.payment.queryInfo(message);
            })),
            signAndSend: (signer, { era /** TODO tip */ }) => {
                const status$ = new rxjs_1.BehaviorSubject(undefined);
                return this.#signingInfo(address, era).pipe((0, rxjs_1.switchMap)((info) => {
                    const payload = {
                        address,
                        blockHash: info.header?.hash.toHex() ?? this.#genesisHash ?? "0x0",
                        blockNumber: info.header?.number.toHex() ?? "0x00000000",
                        era: this.registry
                            .createTypeUnsafe("ExtrinsicEra", [
                            {
                                current: info.header?.number ?? 0,
                                period: era ?? info.mortalLength,
                            },
                        ])
                            .toHex(),
                        genesisHash: this.#genesisHash ?? "0x0",
                        method,
                        nonce: info.nonce.toHex(),
                        signedExtensions: [
                            "CheckNonZeroSender",
                            "CheckSpecVersion",
                            "CheckTxVersion",
                            "CheckGenesis",
                            "CheckMortality",
                            "CheckNonce",
                            "CheckWeight",
                            "ChargeTransactionPayment",
                        ],
                        specVersion: this.#runtimeVersion?.specVersion.toHex() ?? "0x0",
                        tip: "0x00000000000000000000000000000000",
                        transactionVersion: this.#runtimeVersion?.transactionVersion?.toHex() ?? "0x0",
                        version: 4,
                    };
                    return (0, rxjs_1.of)(payload).pipe((0, rxjs_1.switchMap)((p) => signer.signPayload?.(p) ?? (0, rxjs_1.of)(undefined)), (0, rxjs_1.switchMap)((result) => {
                        if (!result) {
                            return (0, rxjs_1.of)(undefined);
                        }
                        const prefix = `${CONSTS.MAGIC_VAR}${this.#noEnum ? "" : "00"}`;
                        const msg = `${prefix}${pub.slice(2)}${result.signature.slice(2)}${payload.era.slice(2)}${this.registry
                            .createType("[Compact<u32>; 2]", [
                            info.nonce.toNumber(),
                            0 /* TODO tip */,
                        ])
                            .toHex()
                            .slice(2)}${method.slice(2)}`;
                        const len = msg.length / 2;
                        const message = this.registry.createType("[Compact<u32>; 1]", [len]).toHex() +
                            msg;
                        return this.rpc.provider.subscribe("author_extrinsicUpdate", "author_submitAndWatchExtrinsic", [message], (err, raw) => {
                            const status = this.registry.createType("ExtrinsicStatus", raw);
                            if (!err) {
                                status$.next(status);
                            }
                            else {
                                console.error(err);
                                // TODO proper error handling
                            }
                        });
                    }));
                }), (0, rxjs_1.combineLatestWith)(status$), (0, rxjs_1.tap)(([subId, status]) => {
                    if (subId &&
                        (status?.isRetracted ||
                            status?.isFinalityTimeout ||
                            status?.isFinalized ||
                            status?.isUsurped ||
                            status?.isDropped ||
                            status?.isInvalid)) {
                        this.rpc.provider.unsubscribe("author_extrinsicUpdate", "author_submitAndWatchExtrinsic", subId);
                    }
                }), (0, rxjs_1.map)(([, status]) => {
                    return status;
                }));
            },
        };
    }
    disconnect() {
        this.rpc.provider.disconnect();
    }
}
exports.default = ApiLite;
