import { RpcCore } from "@polkadot/rpc-core";
import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
import * as CONSTS from "./consts";
import definitions from "./defs";
import { LiteRegistry } from "./registry";
import { blake2Concat } from "./util";
let instanceCounter = 0;
import { BehaviorSubject, combineLatest, combineLatestWith, filter, map, of, switchMap, tap, } from "rxjs";
class ApiLite {
    #instanceId;
    isConnected;
    #connectedSub;
    #genesisHash;
    #runtimeSub;
    #runtimeVersion;
    // eslint-disable-next-line
    #keepaliveInterval;
    #noEnum;
    _debug = false;
    registry = new LiteRegistry();
    rpc;
    get genesisHash() {
        if (!this.#genesisHash) {
            throw new Error("not initialized");
        }
        return this.#genesisHash;
    }
    static create(provider, skipRegistry = false, noEnum = false) {
        const api = new ApiLite(provider, skipRegistry, noEnum);
        return api.isConnected.pipe(filter((v) => !!v), 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 = 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) {
        this.#noEnum = noEnum;
        let ready = false;
        this.#instanceId = `lite:${instanceCounter++}`;
        this.rpc = new RpcCore(this.#instanceId, this.registry, provider);
        this.isConnected = new 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(definitions);
        }
    }
    #signingHeader() {
        return combineLatest([
            this.rpc.chain
                .getHeader()
                .pipe(switchMap((header) => header.parentHash.isEmpty
                ? of(header)
                : this.rpc.chain.getHeader(header.parentHash))),
            this.rpc.chain
                .getFinalizedHead()
                .pipe(switchMap((hash) => this.rpc.chain.getHeader(hash))),
        ]).pipe(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 combineLatest([
            this.rpc.system.accountNextIndex(address),
            !era || (typeof era === "number" && era > 0)
                ? this.#signingHeader()
                : of(null),
        ]).pipe(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,
        })));
    }
    account(addresses) {
        const endpoints = addresses.map((address) => "0x" +
            CONSTS.STORAGE_KEYS.PALLETS.SYSTEM +
            CONSTS.STORAGE_KEYS.METHODS.ACCOUNT +
            blake2Concat(decodeAddress(address)).slice(2));
        return this.rpc.state.subscribeStorage(endpoints).pipe(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(decodeAddress(address)).toString("hex")}`;
    }
    #signAndSend(signer, address, { era /** TODO tip */ }, method) {
        const pub = this.pubByAddress(address);
        const status$ = new BehaviorSubject(undefined);
        return this.#signingInfo(address, era).pipe(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 of(payload).pipe(switchMap((p) => signer.signPayload?.(p) ?? of(undefined)), switchMap((result) => {
                if (!result) {
                    return 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;
                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
                    }
                });
            }));
        }), combineLatestWith(status$), 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);
            }
        }), map(([, status]) => {
            return status;
        }));
    }
    #getSigMessage(address, method) {
        const pub = this.pubByAddress(address);
        return this.#signingInfo(address, undefined).pipe(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;
            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(switchMap((message) => {
                return this.rpc.payment.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(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(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 = 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(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 = encodeAddress(pub /** TODO get ss58 from chain */);
        const getFakeSigMessage = () => this.#signingInfo(address, undefined).pipe(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(switchMap((message) => {
                return this.rpc.payment.queryInfo(message);
            })),
            signAndSend: (signer, { era /** TODO tip */ }) => {
                const status$ = new BehaviorSubject(undefined);
                return this.#signingInfo(address, era).pipe(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 of(payload).pipe(switchMap((p) => signer.signPayload?.(p) ?? of(undefined)), switchMap((result) => {
                        if (!result) {
                            return 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
                            }
                        });
                    }));
                }), combineLatestWith(status$), 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);
                    }
                }), map(([, status]) => {
                    return status;
                }));
            },
        };
    }
    disconnect() {
        this.rpc.provider.disconnect();
    }
}
export default ApiLite;
//# sourceMappingURL=class.js.map