"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ArweaveClient = exports.DEFAULT_ARWEAVE_GATEWAYS = void 0;
const tslib_1 = require("tslib");
const arweave_1 = tslib_1.__importDefault(require("arweave"));
const superstruct_1 = require("superstruct");
const constants_1 = require("../../constants");
const BigNumberUtils_1 = require("../../utils/BigNumberUtils");
const common_1 = require("../../utils/common");
const FetchUtils_1 = require("../../utils/FetchUtils");
const JSONUtils_1 = require("../../utils/JSONUtils");
const TypeGuards_1 = require("../../utils/TypeGuards");
exports.DEFAULT_ARWEAVE_GATEWAYS = [{ host: "arweave.net" }, { host: "ar-io.net" }];
function decodeBase64UrlUtf8(value) {
    return Buffer.from(value, "base64url").toString("utf-8");
}
class ArweaveWriteError extends Error {
    phase;
    gateway;
    status;
    constructor(message, phase, gateway, status) {
        super(message);
        this.phase = phase;
        this.gateway = gateway;
        this.status = status;
        this.name = "ArweaveWriteError";
    }
}
class ArweaveClient {
    arweaveJWT;
    logger;
    retries;
    retryDelaySeconds;
    gateways;
    constructor(arweaveJWT, logger, gateways = exports.DEFAULT_ARWEAVE_GATEWAYS, retries = 2, retryDelaySeconds = 1) {
        this.arweaveJWT = arweaveJWT;
        this.logger = logger;
        this.retries = retries;
        this.retryDelaySeconds = retryDelaySeconds;
        if (gateways.length === 0) {
            throw new Error("At least one gateway must be provided");
        }
        if (this.retries < 0) {
            throw new Error(`retries cannot be < 0 and must be an integer. Currently set to ${this.retries}`);
        }
        if (this.retryDelaySeconds < 0) {
            throw new Error(`delay cannot be < 0. Currently set to ${this.retryDelaySeconds}`);
        }
        this.gateways = gateways.map(({ host, protocol = "https", port = 443 }) => ({
            client: new arweave_1.default({ host, port, protocol, timeout: 20000, logging: false }),
            url: `${protocol}://${host}:${port}`,
        }));
        this.logger.debug({
            at: "ArweaveClient:constructor",
            message: "Arweave client initialized",
            gateways: this.gateways.map((g) => g.url),
        });
    }
    async _raceGateways(label, fn, topicTag) {
        try {
            return await Promise.any(this.gateways.map((gw) => this._retryRequest(() => fn(gw), 0, label, gw.url, topicTag)));
        }
        catch (e) {
            if (e instanceof AggregateError) {
                const details = this.gateways.map((gw, i) => `${gw.url}: ${e.errors[i]}`).join("; ");
                throw new Error(`All Arweave gateways failed for ${label}: ${details}`);
            }
            throw e;
        }
    }
    async _failoverGateways(label, fn, topicTag) {
        const errors = [];
        for (const [index, gw] of this.gateways.entries()) {
            try {
                return await this._retryRequest(() => fn(gw, index + 1), 0, label, gw.url, topicTag);
            }
            catch (e) {
                const error = e instanceof Error ? e : new Error(String(e));
                errors.push(error);
                this.logger.debug({
                    at: "ArweaveClient:failoverGateways",
                    message: `Gateway ${gw.url} failed for ${label}, trying next gateway`,
                    gateway: gw.url,
                    attempt: index + 1,
                    error: String(error),
                });
            }
        }
        const details = this.gateways.map((gw, i) => `${gw.url}: ${errors[i]}`).join("; ");
        throw new Error(`All Arweave gateways failed for ${label}: ${details}`);
    }
    async _retryRequest(request, retryCount, label, gateway, topicTag) {
        try {
            return await request();
        }
        catch (e) {
            if (retryCount < this.retries) {
                const baseDelay = this.retryDelaySeconds * Math.pow(2, retryCount);
                const delayS = baseDelay + baseDelay * Math.random();
                this.logger.debug({
                    at: "ArweaveClient:retryRequest",
                    message: `Arweave request failed, retrying after waiting ${delayS} seconds`,
                    label,
                    gateway,
                    topicTag,
                    retryCount,
                    retryAttempt: retryCount + 1,
                    maxRetries: this.retries,
                    nextRetryDelaySeconds: delayS,
                    error: String(e),
                });
                await (0, common_1.delay)(delayS);
                return this._retryRequest(request, retryCount + 1, label, gateway, topicTag);
            }
            else {
                throw e;
            }
        }
    }
    _isNotFoundError(error) {
        if ((0, FetchUtils_1.isHttpError)(error)) {
            return error.status === 404;
        }
        const message = error instanceof Error ? error.message : String(error);
        return /404/i.test(message);
    }
    _wrapWriteError(error, phase, gateway) {
        if (error instanceof ArweaveWriteError) {
            return error;
        }
        if ((0, FetchUtils_1.isHttpError)(error)) {
            return new ArweaveWriteError(error.message, phase, gateway, error.status);
        }
        const message = error instanceof Error ? error.message : String(error);
        return new ArweaveWriteError(message, phase, gateway);
    }
    async set(value, topicTag) {
        const payload = JSON.stringify(value, JSONUtils_1.jsonReplacerWithBigNumbers);
        let signedTransaction;
        try {
            return await this._failoverGateways("set", async ({ client, url }, attempt) => {
                if (!signedTransaction) {
                    let createdTransaction;
                    try {
                        createdTransaction = await client.createTransaction({ data: payload }, this.arweaveJWT);
                    }
                    catch (error) {
                        throw this._wrapWriteError(error, "createTransaction", url);
                    }
                    createdTransaction.addTag("Content-Type", "application/json");
                    createdTransaction.addTag("App-Name", constants_1.ARWEAVE_TAG_APP_NAME);
                    createdTransaction.addTag("App-Version", constants_1.ARWEAVE_TAG_APP_VERSION.toString());
                    if ((0, TypeGuards_1.isDefined)(topicTag)) {
                        createdTransaction.addTag("Topic", topicTag);
                    }
                    try {
                        await client.transactions.sign(createdTransaction, this.arweaveJWT);
                    }
                    catch (error) {
                        throw this._wrapWriteError(error, "sign", url);
                    }
                    signedTransaction = createdTransaction;
                }
                let result;
                try {
                    result = await client.transactions.post(signedTransaction);
                }
                catch (error) {
                    throw this._wrapWriteError(error, "post", url);
                }
                if (result.status !== 200) {
                    const message = result?.data?.error?.msg ?? result.statusText ?? `HTTP ${result.status}`;
                    throw new ArweaveWriteError(message, "post", url, result.status);
                }
                this.logger.debug({
                    at: "ArweaveClient:set",
                    message: `Arweave transaction posted with ${signedTransaction.id}`,
                    gateway: url,
                    attempt,
                    phase: "post",
                    txn: signedTransaction.id,
                });
                return signedTransaction.id;
            }, topicTag);
        }
        catch (error) {
            this.logger.warn({
                at: "ArweaveClient:set",
                message: "Failed to persist data to Arweave after exhausting all gateways",
                topicTag,
                error: String(error),
            });
            throw error;
        }
    }
    async get(transactionID, validator) {
        const data = await this._raceGateways("get", async ({ url }) => {
            return await (0, FetchUtils_1.fetchWithTimeout)(`${url}/${transactionID}`, {}, {}, 20_000);
        });
        try {
            return (0, superstruct_1.create)(data, validator);
        }
        catch (e) {
            this.logger.warn({
                at: "ArweaveClient:get",
                message: `Retrieved value from Arweave does not match the expected type: ${e}`,
            });
            return null;
        }
    }
    async getByTopic(tag, validator, originQueryAddress = constants_1.DEFAULT_ARWEAVE_STORAGE_ADDRESS) {
        const topicFilter = tag ? `{ name: "Topic", values: ["${tag}"] }` : "";
        const query = `{
      transactions (
        owners: ["${originQueryAddress}"]
        tags: [
          { name: "App-Name", values: ["${constants_1.ARWEAVE_TAG_APP_NAME}"] },
          { name: "Content-Type", values: ["application/json"] },
          { name: "App-Version", values: ["${constants_1.ARWEAVE_TAG_APP_VERSION}"] },
          ${topicFilter}
        ]
      ) { edges { node { id } } }
    }`;
        const response = await this._raceGateways("getByTopic", async ({ url }) => {
            return await (0, FetchUtils_1.postWithTimeout)(`${url}/graphql`, { query }, {}, {}, 20_000);
        }, tag);
        const entries = response?.data?.transactions?.edges ?? [];
        this.logger.debug({
            at: "ArweaveClient:getByTopic",
            message: `Retrieved ${entries.length} matching transactions from Arweave`,
            transactions: entries.map((edge) => edge.node.id),
            metaInformation: {
                tag,
                originQueryAddress,
                appVersion: constants_1.ARWEAVE_TAG_APP_VERSION,
            },
        });
        const failures = [];
        const results = await Promise.all(entries.map(async (edge) => {
            try {
                const data = await this.get(edge.node.id, validator);
                return (0, TypeGuards_1.isDefined)(data)
                    ? {
                        data,
                        hash: edge.node.id,
                    }
                    : null;
            }
            catch (e) {
                failures.push({ hash: edge.node.id, error: e });
                return null;
            }
        }));
        const notFoundFailures = failures.filter(({ error }) => this._isNotFoundError(error));
        const unexpectedFailures = failures.filter(({ error }) => !this._isNotFoundError(error));
        if (notFoundFailures.length > 0) {
            this.logger.debug({
                at: "ArweaveClient:getByTopic",
                message: `Skipped ${notFoundFailures.length} Arweave topic entries that were not yet available`,
                tag,
                transactions: notFoundFailures.map(({ hash }) => hash),
            });
        }
        if (unexpectedFailures.length > 0) {
            this.logger.warn({
                at: "ArweaveClient:getByTopic",
                message: `Failed to fetch ${unexpectedFailures.length} Arweave topic entries`,
                tag,
                failures: unexpectedFailures.map(({ hash, error }) => ({
                    hash,
                    error: String(error),
                })),
            });
        }
        return results.filter(TypeGuards_1.isDefined);
    }
    async getMetadata(transactionID) {
        const transaction = await this._raceGateways("getMetadata", async ({ url }) => {
            return await (0, FetchUtils_1.fetchWithTimeout)(`${url}/tx/${transactionID}`, {}, {}, 20_000);
        });
        if (!(0, TypeGuards_1.isDefined)(transaction)) {
            return null;
        }
        const tags = Object.fromEntries((transaction.tags ?? []).map((tag) => [decodeBase64UrlUtf8(tag.name), decodeBase64UrlUtf8(tag.value)]));
        return {
            contentType: tags["Content-Type"],
            appName: tags["App-Name"],
            topic: tags.Topic,
        };
    }
    getAddress() {
        return this.gateways[0].client.wallets.jwkToAddress(this.arweaveJWT);
    }
    async getBalance() {
        const address = await this.getAddress();
        return this._raceGateways("getBalance", async ({ client }) => {
            const balanceInFloat = await client.wallets.getBalance(address);
            if (balanceInFloat.includes("e")) {
                const [balance, exponent] = balanceInFloat.split("e");
                const resultingBN = BigNumberUtils_1.BigNumber.from(balance).mul((0, BigNumberUtils_1.toBN)(10).pow(exponent.replace("+", "")));
                return BigNumberUtils_1.BigNumber.from(resultingBN.toString());
            }
            else {
                return BigNumberUtils_1.BigNumber.from(balanceInFloat);
            }
        });
    }
}
exports.ArweaveClient = ArweaveClient;
//# sourceMappingURL=ArweaveClient.js.map