oceanprotocol/ocean.js

View on GitHub
src/contracts/NFTFactory.ts

Summary

Maintainability
F
1 wk
Test Coverage
C
79%
import { BigNumber, ethers } from 'ethers'
import ERC721Factory from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json'
import {
  generateDtName,
  ZERO_ADDRESS,
  sendTx,
  getEventFromTx,
  getTokenDecimals,
  LoggerInstance
} from '../utils'
import {
  AbiItem,
  FreCreationParams,
  DatatokenCreateParams,
  DispenserCreationParams,
  NftCreateData,
  Template,
  TokenOrder,
  ReceiptOrEstimate
} from '../@types'
import { SmartContractWithAddress } from './SmartContractWithAddress'

/**
 * Provides an interface for NFT Factory contract
 */
export class NftFactory extends SmartContractWithAddress {
  getDefaultAbi() {
    return ERC721Factory.abi as AbiItem[]
  }

  /**
   * Create new data NFT
   * @param {NFTCreateData} nftData The data needed to create an NFT.
   * @param {Boolean} [estimateGas] if True, return gas estimate
   * @return {Promise<string|BigNumber>} The transaction hash or the gas estimate.
   */
  public async createNFT<G extends boolean = false>(
    nftData: NftCreateData,
    estimateGas?: G
  ): Promise<G extends false ? string : BigNumber> {
    if (!nftData.templateIndex) nftData.templateIndex = 1

    if (!nftData.name || !nftData.symbol) {
      const { name, symbol } = generateDtName()
      nftData.name = name
      nftData.symbol = symbol
    }
    if (nftData.templateIndex > (await this.getCurrentNFTTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (nftData.templateIndex === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }
    if ((await this.getNFTTemplate(nftData.templateIndex)).isActive === false) {
      throw new Error(`Template is not active`)
    }

    const estGas = await this.contract.estimateGas.deployERC721Contract(
      nftData.name,
      nftData.symbol,
      nftData.templateIndex,
      ZERO_ADDRESS,
      ZERO_ADDRESS,
      nftData.tokenURI,
      nftData.transferable,
      nftData.owner
    )
    if (estimateGas) return <G extends false ? string : BigNumber>estGas
    // Invoke createToken function of the contract
    try {
      const tx = await sendTx(
        estGas,
        this.getSignerAccordingSdk(),
        this.config?.gasFeeMultiplier,
        this.contract.deployERC721Contract,
        nftData.name,
        nftData.symbol,
        nftData.templateIndex,
        ZERO_ADDRESS,
        ZERO_ADDRESS,
        nftData.tokenURI,
        nftData.transferable,
        nftData.owner
      )
      if (!tx) {
        const e =
          'Tx for deploying new NFT contract does not exist or status is not successful.'
        console.error(e)
        throw e
      }
      const trxReceipt = await tx.wait()
      const events = getEventFromTx(trxReceipt, 'NFTCreated')
      return events.args[0]
    } catch (e) {
      console.error(`Creation of NFT failed: ${e}`)
    }
  }

  /**
   * Get Current NFT Count (NFT created)
   * @return {Promise<number>} Number of NFT created from this factory
   */
  public async getCurrentNFTCount(): Promise<number> {
    const nftCount = await this.contract.getCurrentNFTCount()
    return nftCount
  }

  /**
   * Get Current Datatoken Count
   * @return {Promise<number>} Number of DTs created from this factory
   */
  public async getCurrentTokenCount(): Promise<number> {
    const tokenCount = await this.contract.getCurrentTokenCount()
    return tokenCount
  }

  /**
   *  Get Factory Owner
   * @return {Promise<string>} Factory Owner address
   */
  public async getOwner(): Promise<string> {
    const owner = await this.contract.owner()
    return owner
  }

  /**
   * Get Current NFT Template Count
   * @return {Promise<number>} Number of NFT Template added to this factory
   */
  public async getCurrentNFTTemplateCount(): Promise<number> {
    const count = await this.contract.getCurrentNFTTemplateCount()
    return count
  }

  /**
   * Get Current Template  Datatoken (ERC20) Count
   * @return {Promise<number>} Number of Datatoken Template added to this factory
   */
  public async getCurrentTokenTemplateCount(): Promise<number> {
    const count = await this.contract.getCurrentTemplateCount()
    return count
  }

  /**
   * Get NFT Template
   * @param {number} index Template index
   * @return {Promise<Template>} Number of Template added to this factory
   */
  public async getNFTTemplate(index: number): Promise<Template> {
    if (index > (await this.getCurrentNFTTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (index === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }
    const template = await this.contract.getNFTTemplate(index)
    return template
  }

  /**
   * Get Datatoken (ERC20) Template
   * @param {number} index Template index
   * @return {Promise<Template>} DT Template info
   */
  public async getTokenTemplate(index: number): Promise<Template> {
    const template = await this.contract.getTokenTemplate(index)
    return template
  }

  /**
   * Check if Datatoken is deployed from the factory
   * @param {String} datatoken Datatoken address to check
   * @return {Promise<Boolean>} return true if deployed from this factory
   */
  public async checkDatatoken(datatoken: string): Promise<Boolean> {
    const isDeployed = await this.contract.erc20List(datatoken)
    return isDeployed
  }

  /**
   * Check if  NFT is deployed from the factory
   * @param {String} nftAddress nftAddress address to check
   * @return {Promise<String>} return address(0) if it's not, or the nftAddress if true
   */
  public async checkNFT(nftAddress: string): Promise<String> {
    const confirmAddress = await this.contract.erc721List(nftAddress)
    return confirmAddress
  }

  /**
   * Add a new NFT token template - only factory Owner
   * @param {String} address caller address
   * @param {String} templateAddress template address to add
   * @param {Boolean} [estimateGas] if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>}
   */
  public async addNFTTemplate<G extends boolean = false>(
    address: string,
    templateAddress: string,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateAddress === ZERO_ADDRESS) {
      throw new Error(`Template cannot be ZERO address`)
    }

    const estGas = await this.contract.estimateGas.add721TokenTemplate(templateAddress)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.add721TokenTemplate,
      templateAddress
    )
    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Disable token template - only factory Owner
   * @param {String} address
   * @param {Number} templateIndex index of the template we want to disable
   * @param {Boolean} estimateGas if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>} current token template count
   */
  public async disableNFTTemplate<G extends boolean = false>(
    address: string,
    templateIndex: number,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateIndex > (await this.getCurrentNFTTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (templateIndex === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }

    const estGas = await this.contract.estimateGas.disable721TokenTemplate(templateIndex)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.disable721TokenTemplate,
      templateIndex
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Reactivate a previously disabled token template - only factory Owner
   * @param {String} address
   * @param {Number} templateIndex index of the template we want to reactivate
   * @param {Boolean} estimateGas if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>} current token template count
   */
  public async reactivateNFTTemplate<G extends boolean = false>(
    address: string,
    templateIndex: number,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateIndex > (await this.getCurrentNFTTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (templateIndex === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }

    const estGas = await this.contract.estimateGas.reactivate721TokenTemplate(
      templateIndex
    )
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.reactivate721TokenTemplate,
      templateIndex
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Add a new NFT token template - only factory Owner
   * @param {String} address caller address
   * @param {String} templateAddress template address to add
   * @param {Boolean} estimateGas if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>}
   */
  public async addTokenTemplate<G extends boolean = false>(
    address: string,
    templateAddress: string,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateAddress === ZERO_ADDRESS) {
      throw new Error(`Template cannot be address ZERO`)
    }

    const estGas = await this.contract.estimateGas.addTokenTemplate(templateAddress)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.addTokenTemplate,
      templateAddress
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Disable token template - only factory Owner
   * @param {String} address caller address
   * @param {Number} templateIndex index of the template we want to disable
   * @param {Boolean} estimateGas if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>} current token template count
   */
  public async disableTokenTemplate<G extends boolean = false>(
    address: string,
    templateIndex: number,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateIndex > (await this.getCurrentTokenTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (templateIndex === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }
    if ((await this.getTokenTemplate(templateIndex)).isActive === false) {
      throw new Error(`Template is already disabled`)
    }

    const estGas = await this.contract.estimateGas.disableTokenTemplate(templateIndex)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.disableTokenTemplate,
      templateIndex
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Reactivate a previously disabled token template - only factory Owner
   * @param {String} address caller address
   * @param {Number} templateIndex index of the template we want to reactivate
   * @param {Boolean} estimateGas if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>} current token template count
   */
  public async reactivateTokenTemplate<G extends boolean = false>(
    address: string,
    templateIndex: number,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if ((await this.getOwner()) !== address) {
      throw new Error(`Caller is not Factory Owner`)
    }
    if (templateIndex > (await this.getCurrentTokenTemplateCount())) {
      throw new Error(`Template index doesnt exist`)
    }

    if (templateIndex === 0) {
      throw new Error(`Template index cannot be ZERO`)
    }
    if ((await this.getTokenTemplate(templateIndex)).isActive === true) {
      throw new Error(`Template is already active`)
    }

    const estGas = await this.contract.estimateGas.reactivateTokenTemplate(templateIndex)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.reactivateTokenTemplate,
      templateIndex
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   *      Used as a proxy to order multiple services
   *      Users can have inifinite approvals for fees for factory instead of having one approval/ Datatoken contract
   *      Requires previous approval of all :
   *          - consumeFeeTokens
   *          - publishMarketFeeTokens
   *          - ERC20 Datatokens
   * @param {TokenOrder[]} orders array of of orders
   * @param {Boolean} [estimateGas] if True, return gas estimate
   * @return {Promise<ReceiptOrEstimate>} transaction receipt
   */
  public async startMultipleTokenOrder<G extends boolean = false>(
    orders: TokenOrder[],
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    if (orders.length > 50) {
      throw new Error(`Too many orders`)
    }

    const estGas = await this.contract.estimateGas.startMultipleTokenOrder(orders)
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.startMultipleTokenOrder,
      orders
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   *  Creates a new NFT, then a datatoken,all in one call
   * @param {NftCreateData} nftCreateData - The data required to create an NFT.
   * @param {DatatokenCreateParams} dtParams - The parameters required to create a datatoken.
   * @param {boolean} [estimateGas] - Whether to return only estimate gas or not.
   * @return {Promise<ReceiptOrEstimate>} transaction receipt
   */

  public async createNftWithDatatoken<G extends boolean = false>(
    nftCreateData: NftCreateData,
    dtParams: DatatokenCreateParams,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    const ercCreateData = await this.getErcCreationParams(dtParams)

    const estGas = await this.contract.estimateGas.createNftWithErc20(
      nftCreateData,
      ercCreateData
    )
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.createNftWithErc20,
      nftCreateData,
      ercCreateData
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Creates an NFT with a datatoken with a fixed rate  all in one call.
   * be aware if Fixed Rate creation fails, you are still going to pay a lot of gas
   * @param {NftCreateData} nftCreateData - The data required to create an NFT.
   * @param {DatatokenCreateParams} dtParams - The parameters required to create a datatoken.
   * @param {FreCreationParams} freParams - The parameters required to create a fixed-rate exchange contract.
   * @param {boolean} [estimateGas] - Whether to return only estimate gas or not.
   * @returns {Promis<ReceiptOrEstimate<G>>}
   */
  public async createNftWithDatatokenWithFixedRate<G extends boolean = false>(
    nftCreateData: NftCreateData,
    dtParams: DatatokenCreateParams,
    freParams: FreCreationParams,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    const ercCreateData = await this.getErcCreationParams(dtParams)
    const fixedData = await this.getFreCreationParams(freParams)

    const estGas = await this.contract.estimateGas.createNftWithErc20WithFixedRate(
      nftCreateData,
      ercCreateData,
      fixedData
    )
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.createNftWithErc20WithFixedRate,
      nftCreateData,
      ercCreateData,
      fixedData
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Creates an NFT with a datatoken with a dispenser in one call.
   * Be aware if Fixed Rate creation fails, you are still going to pay a lot of gas
   * @param {NftCreateData} nftCreateData - The data required to create an NFT.
   * @param {DatatokenCreateParams} dtParams - The parameters required to create a datatoken.
   * @param {DispenserCreationParams} dispenserParams - The parameters required to create a dispenser contract.
   * @param {boolean} [estimateGas] - Whether to estimate gas or not.
   * @returns {Promis<ReceiptOrEstimate<G>>}
   */
  public async createNftWithDatatokenWithDispenser<G extends boolean = false>(
    nftCreateData: NftCreateData,
    dtParams: DatatokenCreateParams,
    dispenserParams: DispenserCreationParams,
    estimateGas?: G
  ): Promise<ReceiptOrEstimate<G>> {
    const ercCreateData = await this.getErcCreationParams(dtParams)

    dispenserParams.maxBalance = await this.amountToUnits(
      null,
      dispenserParams.maxBalance,
      18
    )

    dispenserParams.maxTokens = await this.amountToUnits(
      null,
      dispenserParams.maxTokens,
      18
    )

    const estGas = await this.contract.estimateGas.createNftWithErc20WithDispenser(
      nftCreateData,
      ercCreateData,
      dispenserParams
    )
    if (estimateGas) return <ReceiptOrEstimate<G>>estGas

    const trxReceipt = await sendTx(
      estGas,
      this.getSignerAccordingSdk(),
      this.config?.gasFeeMultiplier,
      this.contract.createNftWithErc20WithDispenser,
      nftCreateData,
      ercCreateData,
      dispenserParams
    )

    return <ReceiptOrEstimate<G>>trxReceipt
  }

  /**
   * Gets the parameters required to create an ERC20 token.
   * @param {DatatokenCreateParams} dtParams - The parameters required to create a datatoken.
   * @returns {Promise<any>}
   */
  private async getErcCreationParams(dtParams: DatatokenCreateParams): Promise<any> {
    let name: string, symbol: string
    // Generate name & symbol if not present
    if (!dtParams.name || !dtParams.symbol) {
      ;({ name, symbol } = generateDtName())
    }

    let feeTokenDecimals = 18
    if (dtParams.feeToken !== ZERO_ADDRESS) {
      try {
        feeTokenDecimals = await getTokenDecimals(this.signer, dtParams.feeToken)
      } catch (error) {
        LoggerInstance.error('getTokenDecimals error', error)
      }
    }

    // common stuff for other templates
    const addresses = [
      dtParams.minter,
      dtParams.paymentCollector,
      dtParams.mpFeeAddress,
      dtParams.feeToken
    ]

    if (dtParams.filesObject) {
      // template 4 only, ignored for others
      if (dtParams.accessListFactory) {
        addresses.push(dtParams.accessListFactory)
      }
      if (dtParams.allowAccessList) {
        addresses.push(dtParams.allowAccessList)
      }

      if (dtParams.denyAccessList) {
        addresses.push(dtParams.denyAccessList)
      }
    }

    return {
      templateIndex: dtParams.templateIndex,
      strings: [dtParams.name || name, dtParams.symbol || symbol],
      addresses,
      uints: [
        await this.amountToUnits(null, dtParams.cap, 18),
        await this.amountToUnits(null, dtParams.feeAmount, feeTokenDecimals)
      ],
      bytess: dtParams.filesObject
        ? [ethers.utils.toUtf8Bytes(JSON.stringify(dtParams.filesObject))]
        : []
    }
  }

  /**
   * Gets the parameters required to create a fixed-rate exchange contract.
   * @param {FreCreationParams} freParams - The parameters required to create a fixed-rate exchange contract.
   * @returns {Promise<any> }
   */
  private async getFreCreationParams(freParams: FreCreationParams): Promise<any> {
    if (!freParams.allowedConsumer) freParams.allowedConsumer = ZERO_ADDRESS
    const withMint = freParams.withMint === false ? 0 : 1

    return {
      fixedPriceAddress: freParams.fixedRateAddress,
      addresses: [
        freParams.baseTokenAddress,
        freParams.owner,
        freParams.marketFeeCollector,
        freParams.allowedConsumer
      ],
      uints: [
        freParams.baseTokenDecimals,
        freParams.datatokenDecimals,
        await this.amountToUnits(null, freParams.fixedRate, 18),
        await this.amountToUnits(null, freParams.marketFee, 18),
        withMint
      ]
    }
  }
}