"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.OpenSeaMarketplace = exports.OpenSeaWrapper = exports.WrapperFactory = void 0;
const units_1 = require("@ethersproject/units");
const opensea_js_1 = require("opensea-js");
const bignumber_1 = require("@ethersproject/bignumber");
const common_1 = require("@bosonprotocol/common");
const abi_1 = require("@ethersproject/abi");
const constants_1 = require("@ethersproject/constants");
const types_1 = require("./types");
const constants_2 = require("@opensea/seaport-js/lib/constants");
const common_2 = require("@bosonprotocol/common");
const interface_1 = require("../seaport/interface");
const handler_1 = require("../erc721/handler");
class WrapperFactory {
    constructor(_contract, _web3Lib) {
        this._contract = _contract;
        this._web3Lib = _web3Lib;
        if (common_1.abis["OpenSeaWrapperFactoryABI"]) {
            this.iface = new abi_1.Interface(common_1.abis["OpenSeaWrapperFactoryABI"]);
        }
        //TODO: check contract address is valid (trying to read contract)?
    }
    create(args) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._web3Lib.sendTransaction({
                to: this._contract,
                data: this.iface.encodeFunctionData("create", [args.voucherContract])
            });
        });
    }
    getWrapper(args) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = yield this._web3Lib.call({
                to: this._contract,
                data: this.iface.encodeFunctionData("getWrapper", [args.voucherContract])
            });
            const [wrapper] = this.iface.decodeFunctionResult("getWrapper", result);
            return wrapper !== constants_1.AddressZero ? wrapper : undefined;
        });
    }
}
exports.WrapperFactory = WrapperFactory;
class OpenSeaWrapper extends types_1.Wrapper {
    constructor(_contract, _web3Lib) {
        super();
        this._contract = _contract;
        this._web3Lib = _web3Lib;
        if (common_1.abis["OpenSeaWrapperABI"]) {
            this.iface = new abi_1.Interface(common_1.abis["OpenSeaWrapperABI"]);
        }
    }
    wrapForAuction(args) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._web3Lib.sendTransaction({
                to: this._contract,
                data: this.iface.encodeFunctionData("wrapForAuction", [args.tokenIds])
            });
        });
    }
    unwrap(args) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._web3Lib.sendTransaction({
                to: this._contract,
                data: this.iface.encodeFunctionData("unwrap", [args.tokenId])
            });
        });
    }
    encodeFinalizeAuction(args) {
        return this.iface.encodeFunctionData("finalizeAuction", [
            args.tokenId,
            args.order
        ]);
    }
    finalizeAuction(args) {
        return __awaiter(this, void 0, void 0, function* () {
            return this._web3Lib.sendTransaction({
                to: this._contract,
                data: this.iface.encodeFunctionData("finalizeAuction", [
                    args.tokenId,
                    args.order
                ])
            });
        });
    }
    get address() {
        return this._contract;
    }
}
exports.OpenSeaWrapper = OpenSeaWrapper;
class OpenSeaMarketplace extends types_1.Marketplace {
    constructor(_type, _handler, _contracts, _feeRecipient, _web3Lib, feeBasisPoint = 50) {
        super(_type);
        this._handler = _handler;
        this._contracts = _contracts;
        this._feeRecipient = _feeRecipient;
        this._web3Lib = _web3Lib;
        this._wrappersMap = new Map();
        this._feeBasisPoint = feeBasisPoint;
        if (this._contracts["openseaWrapper"]) {
            this._wrapperFactory = new WrapperFactory(this._contracts["openseaWrapper"], this._web3Lib);
        }
    }
    createListing(listing) {
        return __awaiter(this, void 0, void 0, function* () {
            const osListing = this.convertListing(listing);
            const osOrder = yield this._handler.createListing(osListing);
            return this.convertOsOrder(osOrder);
        });
    }
    getFees(price) {
        return bignumber_1.BigNumber.from(price).mul(this._feeBasisPoint).div(10000).toString();
    }
    createBidOrder(listing) {
        return __awaiter(this, void 0, void 0, function* () {
            const fees = this.getFees(listing.price);
            const domain = undefined;
            const salt = undefined;
            const quantity = undefined;
            const { nft } = yield this._handler.api.getNFT(listing.asset.contract, listing.asset.tokenId);
            // getNFTItems(nfts: NFT[], quantities: bigint[]): CreateInputItem[]; is a private method in OpenSea SDK, but need to hack it here
            const considerationAssetItems = this._handler["getNFTItems"]([nft], 
            // [BigInt(quantity ?? 1)]
            [BigInt(1)]);
            const considerationFeeItems = [
                {
                    itemType: 1,
                    token: listing.exchangeToken.address,
                    identifierOrCriteria: "0",
                    amount: fees,
                    startAmount: fees,
                    endAmount: fees,
                    recipient: this._feeRecipient
                }
            ];
            const orderParams = {
                offer: [
                    {
                        itemType: 1,
                        token: listing.exchangeToken.address,
                        amount: listing.price,
                        startAmount: listing.price,
                        endAmount: listing.price
                    }
                ],
                consideration: [...considerationAssetItems, ...considerationFeeItems],
                endTime: listing.expirationTime.toString(),
                zone: listing.zone || "0x0000000000000000000000000000000000000000",
                domain,
                // salt: BigInt(salt ?? 0).toString(),
                salt: BigInt(0).toString(),
                restrictedByZone: true,
                allowPartialFills: false
            };
            const { executeAllActions } = yield this._handler.seaport_v1_6.createOrder(orderParams, listing.offerer);
            const order = yield executeAllActions();
            const protocolAddress = listing.protocolAddress || this._contracts.seaport;
            if (!protocolAddress) {
                throw new Error(`Seaport protocol address must be specified in Listing or CoreSDK config`);
            }
            const osOrder = yield this._handler.api.postOrder(order, {
                protocol: "seaport",
                protocolAddress,
                side: types_1.OrderSide.OFFER
            });
            return this.convertOsOrder(osOrder);
        });
    }
    buildAdvancedOrder(asset_1) {
        return __awaiter(this, arguments, void 0, function* (asset, filter = {}) {
            // Assumption: we're fulfilling a Bid Order (don't know if it makes sense with an Ask order)
            const osOrder = yield this._handler.api.getOrder(Object.assign({ assetContractAddress: asset.contract, tokenId: asset.tokenId, side: types_1.OrderSide.OFFER }, filter));
            const fulfillerAddress = asset.withWrapper
                ? asset.contract // If the token is wrapped, the fulfiller is the wrapper contract itself
                : this._contracts.priceDiscoveryClient; // otherwise the address of the PriceDiscoveryClient contract
            const ffd = yield this._handler.api.generateFulfillmentData(fulfillerAddress, osOrder.orderHash, osOrder.protocolAddress, 
            // osOrder.side
            types_1.OrderSide.LISTING // TODO: Hack waiting for Opensea API bug to be fixed. See https://github.com/fermionprotocol/ui/issues/358
            );
            const inputData = ffd.fulfillment_data.transaction
                .input_data;
            return inputData.orders[0];
        });
    }
    generateFulfilmentData(asset) {
        return __awaiter(this, void 0, void 0, function* () {
            const withWrapper = !!asset.withWrapper;
            const wrapper = withWrapper
                ? yield this.getOrCreateVouchersWrapper(asset.contract)
                : undefined;
            // Asumption: we're fulfilling a Bid Order (don't know if it makes sense with an Ask order)
            const osOrder = yield this._handler.api.getOrder({
                assetContractAddress: withWrapper ? wrapper.address : asset.contract,
                tokenId: asset.tokenId,
                side: types_1.OrderSide.OFFER
            });
            const ffd = yield this._handler.api.generateFulfillmentData(withWrapper
                ? wrapper.address // the wrapper will call the fulfilment method (seaport)
                : this._contracts.priceDiscoveryClient, // the priceDiscoveryClient will call the fulfilment method (seaport)
            osOrder.orderHash, osOrder.protocolAddress, osOrder.side);
            const inputData = ffd.fulfillment_data.transaction
                .input_data;
            const price = inputData.orders[1].parameters.consideration[0].startAmount; // offer price minus opensea fees
            let side, priceDiscoveryContract, conduit, priceDiscoveryData;
            if (withWrapper) {
                side = common_2.Side.Wrapper;
                priceDiscoveryContract = wrapper.address;
                conduit = wrapper.address;
                priceDiscoveryData = wrapper.encodeFinalizeAuction({
                    tokenId: asset.tokenId,
                    order: ffd.fulfillment_data.transaction.input_data
                        .orders[0]
                });
            }
            else {
                side = common_2.Side.Bid;
                priceDiscoveryContract = osOrder.protocolAddress; // seaport contract address
                conduit =
                    osOrder.protocolData.parameters.conduitKey === constants_2.NO_CONDUIT
                        ? osOrder.protocolAddress // Seaport is used as conduit
                        : constants_2.OPENSEA_CONDUIT_ADDRESS; // TODO: might not work on mocked networks
                priceDiscoveryData = (0, interface_1.encodeMatchAdvancedOrders)(inputData.orders, inputData.criteriaResolvers, inputData.fulfillments, inputData.recipient);
            }
            return {
                price,
                side,
                priceDiscoveryContract,
                conduit,
                priceDiscoveryData
            };
        });
    }
    /** getOrCreateVouchersWrapper needs to be done at voucher contract level,
     *  before being able to wrap vouchers, then list them on OS **/
    getOrCreateVouchersWrapper(contractAddress) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this._wrapperFactory) {
                throw new Error("WrapperFactory is not initialized");
            }
            // Is the wrapper already cached?
            let wrapper = this._wrappersMap.get(contractAddress);
            if (!wrapper) {
                // Does the wrapper exist on chain?
                let wrapperAddress = yield this._wrapperFactory.getWrapper({
                    voucherContract: contractAddress
                });
                if (!wrapperAddress) {
                    // Create the wrapper on-chain
                    const tx = yield this._wrapperFactory.create({
                        voucherContract: contractAddress
                    });
                    yield tx.wait();
                    wrapperAddress = yield this._wrapperFactory.getWrapper({
                        voucherContract: contractAddress
                    });
                }
                wrapper = new OpenSeaWrapper(wrapperAddress, this._web3Lib);
                // cache the wrapper for next time
                this._wrappersMap.set(contractAddress, wrapper);
            }
            return wrapper;
        });
    }
    isVoucherWrapped(contractAddress, tokenId) {
        return __awaiter(this, void 0, void 0, function* () {
            let wrapper = this._wrappersMap.get(contractAddress);
            if (!wrapper) {
                if (!this._wrapperFactory) {
                    throw new Error("WrapperFactory is not initialized");
                }
                // Does the wrapper exist on chain?
                const wrapperAddress = yield this._wrapperFactory.getWrapper({
                    voucherContract: contractAddress
                });
                if (!wrapperAddress) {
                    // Wrapper contract doesn't exist
                    return { wrapped: false };
                }
                wrapper = new OpenSeaWrapper(wrapperAddress, this._web3Lib);
                // cache the wrapper for next time
                this._wrappersMap.set(contractAddress, wrapper);
            }
            try {
                yield (0, handler_1.ownerOf)({
                    contractAddress: wrapper.address,
                    tokenId,
                    web3Lib: this._web3Lib
                });
            }
            catch (e) {
                // Wrapper contract exists, however the token is not wrapped
                return { wrapped: false, wrapper: wrapper.address };
            }
            return { wrapped: true, wrapper: wrapper.address };
        });
    }
    wrapVouchers(contract, tokenIds) {
        return __awaiter(this, void 0, void 0, function* () {
            const wrapper = yield this.getOrCreateVouchersWrapper(contract);
            return wrapper.wrapForAuction({ tokenIds });
        });
    }
    unwrapVoucher(contract, tokenId) {
        return __awaiter(this, void 0, void 0, function* () {
            const wrapper = yield this.getOrCreateVouchersWrapper(contract);
            return wrapper.unwrap({ tokenId });
        });
    }
    finalizeAuction(asset) {
        return __awaiter(this, void 0, void 0, function* () {
            const wrapper = yield this.getOrCreateVouchersWrapper(asset.contract);
            const osOrder = yield this._handler.api.getOrder({
                assetContractAddress: wrapper.address, // Bid Order must be for the wrapped token
                tokenId: asset.tokenId,
                side: types_1.OrderSide.OFFER
            });
            const ffd = yield this._handler.api.generateFulfillmentData(this._contracts.priceDiscoveryClient, // the address of the PriceDiscoveryClient contract, which will call the fulfilment method
            osOrder.orderHash, osOrder.protocolAddress, osOrder.side);
            const buyerOrder = ffd.fulfillment_data.transaction.input_data
                .orders[0];
            return wrapper.finalizeAuction({
                tokenId: asset.tokenId,
                order: buyerOrder
            });
        });
    }
    cancelOrder(asset, side) {
        return __awaiter(this, void 0, void 0, function* () {
            const signer = yield this._web3Lib.getSignerAddress();
            const osOrder = yield this._handler.api.getOrder({
                assetContractAddress: asset.contract,
                tokenId: asset.tokenId,
                side: side === common_2.Side.Ask ? types_1.OrderSide.LISTING : types_1.OrderSide.OFFER,
                maker: signer
            });
            yield this._handler.cancelOrder({
                order: osOrder,
                accountAddress: osOrder.maker.address
            });
        });
    }
    convertListing(listing) {
        return {
            asset: {
                tokenAddress: listing.asset.contract,
                tokenId: listing.asset.tokenId
            },
            accountAddress: listing.offerer,
            startAmount: (0, units_1.formatUnits)(listing.price, listing.exchangeToken.decimals),
            listingTime: listing.listingTime,
            expirationTime: listing.expirationTime,
            paymentTokenAddress: listing.exchangeToken.address,
            englishAuction: listing.auction
        };
    }
    convertOsOrder(osOrder) {
        if (!osOrder) {
            return undefined;
        }
        const orderInfo = this.extractOrderInfo(osOrder.protocolData.parameters);
        const side = orderInfo.side === types_1.OrderSide.LISTING ? common_2.Side.Ask : common_2.Side.Bid;
        return {
            offerer: orderInfo.offerer,
            side,
            contract: orderInfo.nftContract,
            tokenId: orderInfo.tokenId,
            exchangeToken: {
                address: orderInfo.exchangeToken
                // decimals: ???
            },
            price: orderInfo.price,
            startTime: osOrder.listingTime,
            endTime: osOrder.expirationTime,
            orderHash: osOrder.orderHash,
            protocolAddress: osOrder.protocolAddress,
            maker: osOrder.maker ? { address: osOrder.maker.address } : undefined,
            taker: osOrder.taker ? { address: osOrder.taker.address } : undefined,
            isAuction: osOrder.orderType === opensea_js_1.OrderType.ENGLISH
        };
    }
    extractOrderInfo(parameters, side) {
        var _a, _b;
        let price;
        let exchangeToken;
        let nft;
        const nftBid = parameters.consideration.find((c) => c.itemType === constants_2.ItemType.ERC721);
        const nftAsk = parameters.offer.find((c) => c.itemType === constants_2.ItemType.ERC721);
        if (side === types_1.OrderSide.OFFER && !nftBid) {
            console.warn(`NFT not found in order consideration`);
        }
        if (side === types_1.OrderSide.LISTING && !nftAsk) {
            console.warn(`NFT not found in order offer`);
        }
        if (!side) {
            if (nftBid && !nftAsk) {
                side = types_1.OrderSide.OFFER;
            }
            else if (!nftBid && nftAsk) {
                side = types_1.OrderSide.LISTING;
            }
            else if (nftBid && nftAsk) {
                console.warn(`NFT found in both consideration and offer. Unable to detect the order side`);
            }
            else {
                console.warn(`No NFT found in consideration or offer. Unable to detect the order side`);
            }
        }
        if (side === types_1.OrderSide.OFFER) {
            price = parameters.offer
                .filter((c) => c.itemType === constants_2.ItemType.ERC20)
                .reduce((total, current) => total.add(current.startAmount), bignumber_1.BigNumber.from(0))
                .toString();
            exchangeToken = (_a = parameters.offer.find((c) => c.itemType === constants_2.ItemType.ERC20)) === null || _a === void 0 ? void 0 : _a.token;
            nft = nftBid;
        }
        else {
            // ASK
            price = parameters.consideration
                .filter((c) => c.itemType === constants_2.ItemType.ERC20)
                .reduce((total, current) => total.add(current.startAmount), bignumber_1.BigNumber.from(0))
                .toString();
            exchangeToken = (_b = parameters.consideration.find((c) => c.itemType === constants_2.ItemType.ERC20)) === null || _b === void 0 ? void 0 : _b.token;
            nft = nftAsk;
        }
        const fees = price ? this.getFees(price) : "0";
        const sellerProfit = price
            ? bignumber_1.BigNumber.from(price).sub(fees).toString()
            : "0";
        return {
            offerer: parameters.offerer,
            side,
            nftContract: (nft === null || nft === void 0 ? void 0 : nft.token) || "0x0",
            tokenId: (nft === null || nft === void 0 ? void 0 : nft.identifierOrCriteria) || "0x0",
            price,
            fees,
            sellerProfit,
            exchangeToken
        };
    }
    getOrder(asset_1, side_1) {
        return __awaiter(this, arguments, void 0, function* (asset, side, filter = {}) {
            var _a;
            const osOrder = yield this._handler.api.getOrder(Object.assign({ assetContractAddress: asset.contract, tokenId: asset.tokenId, side: side === common_2.Side.Ask ? types_1.OrderSide.LISTING : types_1.OrderSide.OFFER }, filter));
            return osOrder
                ? Object.assign(Object.assign({}, this.convertOsOrder(osOrder)), { signature: (_a = osOrder.protocolData) === null || _a === void 0 ? void 0 : _a.signature }) : undefined;
        });
    }
    getOrders(asset_1, side_1) {
        return __awaiter(this, arguments, void 0, function* (asset, side, filter = {}) {
            const { orders } = yield this._handler.api.getOrders(Object.assign({ assetContractAddress: asset.contract, tokenIds: asset.tokenIds, side: side === common_2.Side.Ask ? types_1.OrderSide.LISTING : types_1.OrderSide.OFFER }, filter));
            return orders.map((osOrder) => {
                var _a;
                return (Object.assign(Object.assign({}, this.convertOsOrder(osOrder)), { signature: (_a = osOrder.protocolData) === null || _a === void 0 ? void 0 : _a.signature }));
            });
        });
    }
}
exports.OpenSeaMarketplace = OpenSeaMarketplace;
//# sourceMappingURL=opensea.js.map