import { formatUnits } from "@ethersproject/units";
import { OrderType } from "opensea-js";
import { BigNumber } from "@ethersproject/bignumber";
import { abis } from "@bosonprotocol/common";
import { Interface } from "@ethersproject/abi";
import { AddressZero } from "@ethersproject/constants";
import { Marketplace, OrderSide, Wrapper } from "./types";
import { ItemType, NO_CONDUIT, OPENSEA_CONDUIT_ADDRESS } from "@opensea/seaport-js/lib/constants";
import { Side } from "@bosonprotocol/common";
import { encodeMatchAdvancedOrders } from "../seaport/interface";
import { ownerOf as erc721OwnerOf } from "../erc721/handler";
export class WrapperFactory {
    _contract;
    _web3Lib;
    iface;
    constructor(_contract, _web3Lib) {
        this._contract = _contract;
        this._web3Lib = _web3Lib;
        if (abis["OpenSeaWrapperFactoryABI"]) {
            this.iface = new Interface(abis["OpenSeaWrapperFactoryABI"]);
        }
        //TODO: check contract address is valid (trying to read contract)?
    }
    async create(args) {
        return this._web3Lib.sendTransaction({
            to: this._contract,
            data: this.iface.encodeFunctionData("create", [args.voucherContract])
        });
    }
    async getWrapper(args) {
        const result = await this._web3Lib.call({
            to: this._contract,
            data: this.iface.encodeFunctionData("getWrapper", [args.voucherContract])
        });
        const [wrapper] = this.iface.decodeFunctionResult("getWrapper", result);
        return wrapper !== AddressZero ? wrapper : undefined;
    }
}
export class OpenSeaWrapper extends Wrapper {
    _contract;
    _web3Lib;
    iface;
    constructor(_contract, _web3Lib) {
        super();
        this._contract = _contract;
        this._web3Lib = _web3Lib;
        if (abis["OpenSeaWrapperABI"]) {
            this.iface = new Interface(abis["OpenSeaWrapperABI"]);
        }
    }
    async wrapForAuction(args) {
        return this._web3Lib.sendTransaction({
            to: this._contract,
            data: this.iface.encodeFunctionData("wrapForAuction", [args.tokenIds])
        });
    }
    async unwrap(args) {
        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
        ]);
    }
    async finalizeAuction(args) {
        return this._web3Lib.sendTransaction({
            to: this._contract,
            data: this.iface.encodeFunctionData("finalizeAuction", [
                args.tokenId,
                args.order
            ])
        });
    }
    get address() {
        return this._contract;
    }
}
export class OpenSeaMarketplace extends Marketplace {
    _handler;
    _contracts;
    _feeRecipient;
    _web3Lib;
    _wrapperFactory;
    _wrappersMap = new Map();
    _feeBasisPoint;
    constructor(_type, _handler, _contracts, _feeRecipient, _web3Lib, feeBasisPoint = 50) {
        super(_type);
        this._handler = _handler;
        this._contracts = _contracts;
        this._feeRecipient = _feeRecipient;
        this._web3Lib = _web3Lib;
        this._feeBasisPoint = feeBasisPoint;
        if (this._contracts["openseaWrapper"]) {
            this._wrapperFactory = new WrapperFactory(this._contracts["openseaWrapper"], this._web3Lib);
        }
    }
    async createListing(listing) {
        const osListing = this.convertListing(listing);
        const osOrder = await this._handler.createListing(osListing);
        return this.convertOsOrder(osOrder);
    }
    getFees(price) {
        return BigNumber.from(price).mul(this._feeBasisPoint).div(10000).toString();
    }
    async createBidOrder(listing) {
        const fees = this.getFees(listing.price);
        const domain = undefined;
        const salt = undefined;
        const quantity = undefined;
        const { nft } = await 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 } = await this._handler.seaport_v1_6.createOrder(orderParams, listing.offerer);
        const order = await 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 = await this._handler.api.postOrder(order, {
            protocol: "seaport",
            protocolAddress,
            side: OrderSide.OFFER
        });
        return this.convertOsOrder(osOrder);
    }
    async buildAdvancedOrder(asset, filter = {}) {
        // Assumption: we're fulfilling a Bid Order (don't know if it makes sense with an Ask order)
        const osOrder = await this._handler.api.getOrder({
            assetContractAddress: asset.contract,
            tokenId: asset.tokenId,
            side: 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 = await this._handler.api.generateFulfillmentData(fulfillerAddress, osOrder.orderHash, osOrder.protocolAddress, 
        // osOrder.side
        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];
    }
    async generateFulfilmentData(asset) {
        const withWrapper = !!asset.withWrapper;
        const wrapper = withWrapper
            ? await 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 = await this._handler.api.getOrder({
            assetContractAddress: withWrapper ? wrapper.address : asset.contract,
            tokenId: asset.tokenId,
            side: OrderSide.OFFER
        });
        const ffd = await 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 = 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 = Side.Bid;
            priceDiscoveryContract = osOrder.protocolAddress; // seaport contract address
            conduit =
                osOrder.protocolData.parameters.conduitKey === NO_CONDUIT
                    ? osOrder.protocolAddress // Seaport is used as conduit
                    : OPENSEA_CONDUIT_ADDRESS; // TODO: might not work on mocked networks
            priceDiscoveryData = 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 **/
    async getOrCreateVouchersWrapper(contractAddress) {
        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 = await this._wrapperFactory.getWrapper({
                voucherContract: contractAddress
            });
            if (!wrapperAddress) {
                // Create the wrapper on-chain
                const tx = await this._wrapperFactory.create({
                    voucherContract: contractAddress
                });
                await tx.wait();
                wrapperAddress = await this._wrapperFactory.getWrapper({
                    voucherContract: contractAddress
                });
            }
            wrapper = new OpenSeaWrapper(wrapperAddress, this._web3Lib);
            // cache the wrapper for next time
            this._wrappersMap.set(contractAddress, wrapper);
        }
        return wrapper;
    }
    async isVoucherWrapped(contractAddress, tokenId) {
        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 = await 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 {
            await erc721OwnerOf({
                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 };
    }
    async wrapVouchers(contract, tokenIds) {
        const wrapper = await this.getOrCreateVouchersWrapper(contract);
        return wrapper.wrapForAuction({ tokenIds });
    }
    async unwrapVoucher(contract, tokenId) {
        const wrapper = await this.getOrCreateVouchersWrapper(contract);
        return wrapper.unwrap({ tokenId });
    }
    async finalizeAuction(asset) {
        const wrapper = await this.getOrCreateVouchersWrapper(asset.contract);
        const osOrder = await this._handler.api.getOrder({
            assetContractAddress: wrapper.address, // Bid Order must be for the wrapped token
            tokenId: asset.tokenId,
            side: OrderSide.OFFER
        });
        const ffd = await 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
        });
    }
    async cancelOrder(asset, side) {
        const signer = await this._web3Lib.getSignerAddress();
        const osOrder = await this._handler.api.getOrder({
            assetContractAddress: asset.contract,
            tokenId: asset.tokenId,
            side: side === Side.Ask ? OrderSide.LISTING : OrderSide.OFFER,
            maker: signer
        });
        await 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: 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 === OrderSide.LISTING ? Side.Ask : 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 === OrderType.ENGLISH
        };
    }
    extractOrderInfo(parameters, side) {
        let price;
        let exchangeToken;
        let nft;
        const nftBid = parameters.consideration.find((c) => c.itemType === ItemType.ERC721);
        const nftAsk = parameters.offer.find((c) => c.itemType === ItemType.ERC721);
        if (side === OrderSide.OFFER && !nftBid) {
            console.warn(`NFT not found in order consideration`);
        }
        if (side === OrderSide.LISTING && !nftAsk) {
            console.warn(`NFT not found in order offer`);
        }
        if (!side) {
            if (nftBid && !nftAsk) {
                side = OrderSide.OFFER;
            }
            else if (!nftBid && nftAsk) {
                side = 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 === OrderSide.OFFER) {
            price = parameters.offer
                .filter((c) => c.itemType === ItemType.ERC20)
                .reduce((total, current) => total.add(current.startAmount), BigNumber.from(0))
                .toString();
            exchangeToken = parameters.offer.find((c) => c.itemType === ItemType.ERC20)?.token;
            nft = nftBid;
        }
        else {
            // ASK
            price = parameters.consideration
                .filter((c) => c.itemType === ItemType.ERC20)
                .reduce((total, current) => total.add(current.startAmount), BigNumber.from(0))
                .toString();
            exchangeToken = parameters.consideration.find((c) => c.itemType === ItemType.ERC20)?.token;
            nft = nftAsk;
        }
        const fees = price ? this.getFees(price) : "0";
        const sellerProfit = price
            ? BigNumber.from(price).sub(fees).toString()
            : "0";
        return {
            offerer: parameters.offerer,
            side,
            nftContract: nft?.token || "0x0",
            tokenId: nft?.identifierOrCriteria || "0x0",
            price,
            fees,
            sellerProfit,
            exchangeToken
        };
    }
    async getOrder(asset, side, filter = {}) {
        const osOrder = await this._handler.api.getOrder({
            assetContractAddress: asset.contract,
            tokenId: asset.tokenId,
            side: side === Side.Ask ? OrderSide.LISTING : OrderSide.OFFER,
            ...filter
        });
        return osOrder
            ? {
                ...this.convertOsOrder(osOrder),
                signature: osOrder.protocolData?.signature
            }
            : undefined;
    }
    async getOrders(asset, side, filter = {}) {
        const { orders } = await this._handler.api.getOrders({
            assetContractAddress: asset.contract,
            tokenIds: asset.tokenIds,
            side: side === Side.Ask ? OrderSide.LISTING : OrderSide.OFFER,
            ...filter
        });
        return orders.map((osOrder) => ({
            ...this.convertOsOrder(osOrder),
            signature: osOrder.protocolData?.signature
        }));
    }
}
//# sourceMappingURL=opensea.js.map