"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkOfferIsCommittable = checkOfferIsCommittable;
exports.commitToOffer = commitToOffer;
exports.commitToBuyerOffer = commitToBuyerOffer;
exports.commitToConditionalOffer = commitToConditionalOffer;
exports.createOfferAndCommit = createOfferAndCommit;
exports.isFullOfferVoided = isFullOfferVoided;
exports.signFullOffer = signFullOffer;
exports.completeExchange = completeExchange;
exports.completeExchangeBatch = completeExchangeBatch;
exports.revokeVoucher = revokeVoucher;
exports.cancelVoucher = cancelVoucher;
exports.expireVoucher = expireVoucher;
exports.redeemVoucher = redeemVoucher;
exports.getExchangeTokenId = getExchangeTokenId;
exports.parseTokenId = parseTokenId;
const hash_1 = require("@ethersproject/hash");
const bignumber_1 = require("@ethersproject/bignumber");
const constants_1 = require("@ethersproject/constants");
const common_1 = require("@bosonprotocol/common");
const interface_1 = require("./interface");
const subgraph_1 = require("../offers/subgraph");
const subgraph_2 = require("../exchanges/subgraph");
const subgraph_3 = require("../subgraph");
const handler_1 = require("../erc20/handler");
const subgraph_4 = require("../accounts/subgraph");
const storage_1 = require("../offers/storage");
const storeMetadataItems_1 = require("../metadata/storeMetadataItems");
const signature_1 = require("../utils/signature");
const interface_2 = require("../offers/interface");
function checkOfferIsCommittable(offerId, offer) {
    if (!offer) {
        throw new Error(`Offer with id ${offerId.toString()} does not exist`);
    }
    if (offer.voidedAt) {
        throw new Error(`Offer with id ${offerId.toString()} has been voided`);
    }
    if (Date.now() < Number(offer.validFromDate) * 1000) {
        throw new Error(`Offer with id ${offerId.toString()} is not valid yet`);
    }
    if (Date.now() >= Number(offer.validUntilDate) * 1000) {
        throw new Error(`Offer with id ${offerId.toString()} is not valid anymore`);
    }
    if (Number(offer.quantityAvailable) === 0) {
        throw new Error(`Offer with id ${offerId.toString()} is sold out`);
    }
}
// Implementation
function commitToOffer(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const offer = yield (0, subgraph_1.getOfferById)(args.subgraphUrl, args.offerId);
        yield checkOfferIsCommittable(args.offerId, offer);
        if (offer.creator !== common_1.OfferCreator.Seller) {
            throw new Error(`Offer with id ${args.offerId.toString()} is not seller initiated`);
        }
        if (offer.condition) {
            // keep compatibility with previous version
            if (args.returnTxInfo) {
                return commitToConditionalOffer(Object.assign(Object.assign({}, args), { returnTxInfo: true, tokenId: offer.condition.minTokenId }));
            }
            return commitToConditionalOffer(Object.assign(Object.assign({}, args), { returnTxInfo: false, tokenId: offer.condition.minTokenId }));
        }
        if (offer.exchangeToken.address !== constants_1.AddressZero) {
            const owner = yield args.web3Lib.getSignerAddress();
            // check if we need the committer to approve the token first
            yield (0, handler_1.ensureAllowance)({
                owner,
                spender: args.contractAddress,
                contractAddress: offer.exchangeToken.address,
                value: offer.price,
                web3Lib: args.web3Lib,
                returnTxInfo: args.returnTxInfo
            });
        }
        const transactionRequest = {
            from: args.buyer,
            to: args.contractAddress,
            data: (0, interface_1.encodeCommitToOffer)(args.buyer, args.offerId),
            value: offer.exchangeToken.address === constants_1.AddressZero ? offer.price : "0"
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function commitToBuyerOffer(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const offer = yield (0, subgraph_1.getOfferById)(args.subgraphUrl, args.offerId);
        yield checkOfferIsCommittable(args.offerId, offer);
        if (offer.creator !== common_1.OfferCreator.Buyer) {
            throw new Error(`Offer with id ${args.offerId.toString()} is not buyer initiated`);
        }
        const caller = yield args.web3Lib.getSignerAddress();
        if (offer.exchangeToken.address !== constants_1.AddressZero) {
            // check if we need the committer to approve the token first
            yield (0, handler_1.ensureAllowance)({
                owner: caller,
                spender: args.contractAddress,
                contractAddress: offer.exchangeToken.address,
                value: offer.price,
                web3Lib: args.web3Lib,
                returnTxInfo: args.returnTxInfo
            });
        }
        const transactionRequest = {
            from: caller,
            to: args.contractAddress,
            data: (0, interface_1.encodeCommitToBuyerOffer)(args.offerId, args.sellerParams),
            value: offer.exchangeToken.address === constants_1.AddressZero ? offer.sellerDeposit : "0"
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function commitToConditionalOffer(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const offer = yield (0, subgraph_1.getOfferById)(args.subgraphUrl, args.offerId);
        yield checkOfferIsCommittable(args.offerId, offer);
        if (offer.exchangeToken.address !== constants_1.AddressZero) {
            const owner = yield args.web3Lib.getSignerAddress();
            // check if we need the committer to approve the token first
            yield (0, handler_1.ensureAllowance)({
                owner,
                spender: args.contractAddress,
                contractAddress: offer.exchangeToken.address,
                value: offer.price,
                web3Lib: args.web3Lib,
                returnTxInfo: args.returnTxInfo
            });
        }
        const transactionRequest = {
            from: args.buyer,
            to: args.contractAddress,
            data: (0, interface_1.encodeCommitToConditionalOffer)(args.buyer, args.offerId, args.tokenId),
            value: offer.exchangeToken.address === constants_1.AddressZero ? offer.price : "0"
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function createOfferAndCommit(args) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            common_1.utils.validation.createOfferAndCommitArgsSchema.validateSync(args.createOfferAndCommitArgs, {
                abortEarly: false
            });
        }
        catch (error) {
            console.error("error", error);
            throw error;
        }
        const { disputeResolverId, exchangeToken, price, sellerDeposit, creator } = args.createOfferAndCommitArgs;
        const disputeResolver = yield (0, subgraph_4.getDisputeResolverById)(args.subgraphUrl, disputeResolverId);
        if (!disputeResolver) {
            throw new Error(`Dispute resolver with id "${disputeResolverId}" does not exist`);
        }
        if (!disputeResolver.fees.some((fee) => fee.token.address.toLowerCase() === exchangeToken.toLowerCase())) {
            throw new Error(`Dispute resolver with id "${disputeResolverId}" does not support exchange token "${exchangeToken}"`);
        }
        // check the offer is not voided
        if (yield isFullOfferVoided(Object.assign(Object.assign({}, args), { fullOfferArgsUnsigned: args.createOfferAndCommitArgs }))) {
            throw new Error(`The offer has been voided`);
        }
        yield (0, storage_1.storeMetadataOnTheGraph)({
            metadataUriOrHash: args.createOfferAndCommitArgs.metadataUri,
            metadataStorage: args.metadataStorage,
            theGraphStorage: args.theGraphStorage
        });
        yield (0, storeMetadataItems_1.storeMetadataItems)(Object.assign(Object.assign({}, args), { createOffersArgs: [args.createOfferAndCommitArgs] }));
        const committerPayment = creator === common_1.OfferCreator.Buyer ? sellerDeposit : price;
        if (exchangeToken !== constants_1.AddressZero) {
            const owner = yield args.web3Lib.getSignerAddress();
            // check if we need the committer to approve the token first
            yield (0, handler_1.ensureAllowance)({
                owner,
                spender: args.contractAddress,
                contractAddress: exchangeToken,
                value: committerPayment,
                web3Lib: args.web3Lib,
                returnTxInfo: args.returnTxInfo
            });
        }
        const transactionRequest = Object.assign(Object.assign({}, args.txRequest), { to: args.contractAddress, data: (0, interface_1.encodeCreateOfferAndCommit)(args.createOfferAndCommitArgs), value: exchangeToken === constants_1.AddressZero ? committerPayment : "0" });
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            const txResponse = yield args.web3Lib.sendTransaction(transactionRequest);
            return txResponse;
        }
    });
}
function isFullOfferVoided(args) {
    return __awaiter(this, void 0, void 0, function* () {
        // Compute the offer hash
        const structuredData = yield signFullOffer(Object.assign(Object.assign({}, args), { chainId: yield args.web3Lib.getChainId(), returnTypedDataToSign: true }));
        if (structuredData.types && structuredData.types.EIP712Domain) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            delete structuredData.types.EIP712Domain; // we don't need to hash the domain
        }
        const offerHash = hash_1._TypedDataEncoder.hashStruct("FullOffer", structuredData.types, structuredData.message);
        // Check NonListedOfferVoided events for this offer hash
        const [nonListedOfferVoided] = yield (0, subgraph_2.getNonListedOfferVoided)(args.subgraphUrl, {
            nonListedOfferVoidedsFilter: { id: offerHash }
        });
        return !!nonListedOfferVoided;
    });
}
function signFullOffer(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const offerStruct = (0, interface_2.argsToOfferStruct)(args.fullOfferArgsUnsigned);
        const offerDatesStruct = (0, interface_2.argsToOfferDatesStruct)(args.fullOfferArgsUnsigned);
        const offerDurationsStruct = (0, interface_2.argsToOfferDurationsStruct)(args.fullOfferArgsUnsigned);
        const drParametersStruct = (0, interface_2.argsToDRParametersStruct)(args.fullOfferArgsUnsigned);
        const customSignatureType = {
            FullOffer: [
                { name: "offer", type: "Offer" },
                { name: "offerDates", type: "OfferDates" },
                { name: "offerDurations", type: "OfferDurations" },
                { name: "drParameters", type: "DRParameters" },
                { name: "condition", type: "Condition" },
                { name: "agentId", type: "uint256" },
                { name: "feeLimit", type: "uint256" },
                { name: "useDepositedFunds", type: "bool" }
            ],
            Condition: [
                { name: "method", type: "uint8" },
                { name: "tokenType", type: "uint8" },
                { name: "tokenAddress", type: "address" },
                { name: "gating", type: "uint8" },
                { name: "minTokenId", type: "uint256" },
                { name: "threshold", type: "uint256" },
                { name: "maxCommits", type: "uint256" },
                { name: "maxTokenId", type: "uint256" }
            ],
            DRParameters: [
                { name: "disputeResolverId", type: "uint256" },
                { name: "mutualizerAddress", type: "address" }
            ],
            OfferDurations: [
                { name: "disputePeriod", type: "uint256" },
                { name: "voucherValid", type: "uint256" },
                { name: "resolutionPeriod", type: "uint256" }
            ],
            OfferDates: [
                { name: "validFrom", type: "uint256" },
                { name: "validUntil", type: "uint256" },
                { name: "voucherRedeemableFrom", type: "uint256" },
                { name: "voucherRedeemableUntil", type: "uint256" }
            ],
            Offer: [
                { name: "sellerId", type: "uint256" },
                { name: "price", type: "uint256" },
                { name: "sellerDeposit", type: "uint256" },
                { name: "buyerCancelPenalty", type: "uint256" },
                { name: "quantityAvailable", type: "uint256" },
                { name: "exchangeToken", type: "address" },
                { name: "metadataUri", type: "string" },
                { name: "metadataHash", type: "string" },
                { name: "collectionIndex", type: "uint256" },
                { name: "royaltyInfo", type: "RoyaltyInfo" },
                { name: "creator", type: "uint8" },
                { name: "buyerId", type: "uint256" }
            ],
            RoyaltyInfo: [
                { name: "recipients", type: "address[]" },
                { name: "bps", type: "uint256[]" }
            ]
        };
        const message = {
            offer: {
                sellerId: offerStruct.sellerId.toString(),
                price: offerStruct.price.toString(),
                sellerDeposit: offerStruct.sellerDeposit.toString(),
                buyerCancelPenalty: offerStruct.buyerCancelPenalty.toString(),
                quantityAvailable: offerStruct.quantityAvailable.toString(),
                exchangeToken: offerStruct.exchangeToken,
                metadataUri: offerStruct.metadataUri,
                metadataHash: offerStruct.metadataHash,
                collectionIndex: offerStruct.collectionIndex.toString(),
                royaltyInfo: {
                    recipients: offerStruct.royaltyInfo[0].recipients,
                    bps: offerStruct.royaltyInfo[0].bps.map((bp) => bp.toString())
                },
                creator: offerStruct.creator,
                buyerId: offerStruct.buyerId.toString()
            },
            offerDates: {
                validFrom: offerDatesStruct.validFrom.toString(),
                validUntil: offerDatesStruct.validUntil.toString(),
                voucherRedeemableFrom: offerDatesStruct.voucherRedeemableFrom.toString(),
                voucherRedeemableUntil: offerDatesStruct.voucherRedeemableUntil.toString()
            },
            offerDurations: {
                disputePeriod: offerDurationsStruct.disputePeriod.toString(),
                voucherValid: offerDurationsStruct.voucherValid.toString(),
                resolutionPeriod: offerDurationsStruct.resolutionPeriod.toString()
            },
            drParameters: {
                disputeResolverId: drParametersStruct.disputeResolverId.toString(),
                mutualizerAddress: drParametersStruct.mutualizerAddress
            },
            condition: {
                method: args.fullOfferArgsUnsigned.condition.method,
                tokenType: args.fullOfferArgsUnsigned.condition.tokenType,
                tokenAddress: args.fullOfferArgsUnsigned.condition.tokenAddress,
                gating: args.fullOfferArgsUnsigned.condition.gatingType,
                minTokenId: args.fullOfferArgsUnsigned.condition.minTokenId.toString(),
                threshold: args.fullOfferArgsUnsigned.condition.threshold.toString(),
                maxCommits: args.fullOfferArgsUnsigned.condition.maxCommits.toString(),
                maxTokenId: args.fullOfferArgsUnsigned.condition.maxTokenId.toString()
            },
            agentId: args.fullOfferArgsUnsigned.agentId.toString(),
            feeLimit: args.fullOfferArgsUnsigned.feeLimit.toString(),
            useDepositedFunds: args.fullOfferArgsUnsigned.useDepositedFunds
        };
        const signatureArgs = {
            message,
            customSignatureType,
            web3Lib: args.web3Lib,
            verifyingContractAddress: args.contractAddress,
            chainId: args.chainId,
            primaryType: "FullOffer"
        };
        if (args.returnTypedDataToSign) {
            return (0, signature_1.prepareDataSignatureParameters)(Object.assign(Object.assign({}, signatureArgs), { returnTypedDataToSign: true }));
        }
        else {
            return (0, signature_1.prepareDataSignatureParameters)(Object.assign(Object.assign({}, signatureArgs), { returnTypedDataToSign: false }));
        }
    });
}
// Implementation
function completeExchange(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const [exchange, signerAddress] = yield Promise.all([
            (0, subgraph_2.getExchangeById)(args.subgraphUrl, args.exchangeId),
            args.web3Lib.getSignerAddress()
        ]);
        yield assertCompletableExchange(args.exchangeId, exchange, signerAddress, args.web3Lib);
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeCompleteExchange)(args.exchangeId)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function completeExchangeBatch(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const [exchanges, signerAddress] = yield Promise.all([
            (0, subgraph_2.getExchanges)(args.subgraphUrl, {
                exchangesFilter: { id_in: args.exchangeIds.map((id) => id.toString()) }
            }),
            args.web3Lib.getSignerAddress()
        ]);
        for (const exchange of exchanges) {
            yield assertCompletableExchange(exchange.id, exchange, signerAddress, args.web3Lib);
        }
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeCompleteExchangeBatch)(args.exchangeIds)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function revokeVoucher(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const [exchange, signerAddress] = yield Promise.all([
            (0, subgraph_2.getExchangeById)(args.subgraphUrl, args.exchangeId),
            args.web3Lib.getSignerAddress()
        ]);
        assertExchange(args.exchangeId, exchange);
        assertExchangeState(exchange, subgraph_3.ExchangeState.COMMITTED);
        assertSignerIsAssistant(signerAddress, exchange);
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeRevokeVoucher)(args.exchangeId)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function cancelVoucher(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const [exchange, signerAddress] = yield Promise.all([
            (0, subgraph_2.getExchangeById)(args.subgraphUrl, args.exchangeId),
            args.web3Lib.getSignerAddress()
        ]);
        assertExchange(args.exchangeId, exchange);
        assertExchangeState(exchange, subgraph_3.ExchangeState.COMMITTED);
        assertSignerIsBuyer(signerAddress, exchange);
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeCancelVoucher)(args.exchangeId)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function expireVoucher(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const exchange = yield (0, subgraph_2.getExchangeById)(args.subgraphUrl, args.exchangeId);
        assertExchange(args.exchangeId, exchange);
        if (Date.now() < Number(exchange.validUntilDate) * 1000) {
            throw new Error(`Voucher is still valid`);
        }
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeExpireVoucher)(args.exchangeId)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
// Implementation
function redeemVoucher(args) {
    return __awaiter(this, void 0, void 0, function* () {
        const [exchange, signerAddress] = yield Promise.all([
            (0, subgraph_2.getExchangeById)(args.subgraphUrl, args.exchangeId),
            args.web3Lib.getSignerAddress()
        ]);
        assertExchange(args.exchangeId, exchange);
        assertExchangeState(exchange, subgraph_3.ExchangeState.COMMITTED);
        assertSignerIsBuyer(signerAddress, exchange);
        if (Date.now() < Number(exchange.offer.voucherRedeemableFromDate) * 1000) {
            throw new Error(`Voucher not redeemable yet`);
        }
        if (Date.now() > Number(exchange.validUntilDate) * 1000) {
            throw new Error(`Voucher can not be redeemed anymore`);
        }
        const transactionRequest = {
            to: args.contractAddress,
            data: (0, interface_1.encodeRedeemVoucher)(args.exchangeId)
        };
        if (args.returnTxInfo) {
            return transactionRequest;
        }
        else {
            return args.web3Lib.sendTransaction(transactionRequest);
        }
    });
}
function assertExchange(exchangeId, exchange) {
    if (!exchange) {
        throw new Error(`Exchange with id ${exchangeId} does not exist`);
    }
}
function assertExchangeState(exchange, requiredState) {
    if (exchange.state !== requiredState) {
        throw new Error(`Exchange with id ${exchange.id} not in required state: ${requiredState}, actual state: ${exchange.state}`);
    }
}
function assertSignerIsAssistant(signer, exchange) {
    if (exchange.seller.assistant.toLowerCase() !== signer.toLowerCase()) {
        throw new Error(`Signer ${signer} is not the assistant ${exchange.seller.assistant}`);
    }
}
function assertSignerIsBuyer(signer, exchange) {
    if (exchange.buyer.wallet.toLowerCase() !== signer.toLowerCase()) {
        throw new Error(`Signer ${signer} is not the buyer ${exchange.buyer.wallet}`);
    }
}
function assertSignerIsBuyerOrAssistant(signer, exchange) {
    const { seller, buyer } = exchange;
    const buyerAddress = buyer.wallet;
    const assistantAddress = seller.assistant;
    const isSignerAssistant = signer.toLowerCase() === assistantAddress.toLowerCase();
    const isSignerBuyer = signer.toLowerCase() === buyerAddress.toLowerCase();
    if (!isSignerAssistant && !isSignerBuyer) {
        throw new Error(`Signer ${signer} is required to be the buyer ${buyerAddress} or assistant ${assistantAddress}`);
    }
    return { isSignerBuyer, isSignerAssistant };
}
function assertCompletableExchange(exchangeId, exchange, signer, web3Lib) {
    return __awaiter(this, void 0, void 0, function* () {
        assertExchange(exchangeId, exchange);
        const { isSignerAssistant, isSignerBuyer } = assertSignerIsBuyerOrAssistant(signer, exchange);
        if (isSignerAssistant && !isSignerBuyer) {
            const now = yield web3Lib.getCurrentTimeMs();
            const elapsedSinceRedeemMS = now - Number(exchange.redeemedDate || "0") * 1000;
            const didDisputePeriodElapse = elapsedSinceRedeemMS >
                Number(exchange.offer.disputePeriodDuration) * 1000;
            if (!didDisputePeriodElapse) {
                throw new Error(`Fulfillment period of ${Number(exchange.offer.disputePeriodDuration) * 1000} ms did not elapsed since redeem.`);
            }
        }
    });
}
function getExchangeTokenId(exchangeId, offerId) {
    // Since v2.2.1, tokenId = exchangeId | offerId << 128
    return bignumber_1.BigNumber.from(offerId).shl(128).add(exchangeId);
}
function parseTokenId(tokenId) {
    const offerId = bignumber_1.BigNumber.from(tokenId).shr(128);
    const exchangeId = bignumber_1.BigNumber.from(tokenId).mask(128);
    return { offerId, exchangeId };
}
//# sourceMappingURL=handler.js.map