DeveryERC721.js

import AbstractSmartContract from './AbstractSmartContract';

const deveryERC721Artifact = require('../build/contracts/DeveryERC721MetadataToken.json');
const metadataDeveryERC721WrapperArtifact = require('../build/contracts/MetadataDeveryERC721Wrapper.json');
const ethers = require('ethers');

/**
 *
 * Main class to deal with the owned smart contract interface and related operations,
 * you can use it to check the current contract owner and listen to ownership change related
 * events
 *
 * @version 1
 * @extends AbstractSmartContract
 */
class DeveryERC721 extends AbstractSmartContract {
  /**
   *
   * Creates a new instansce of DeveryERC721.
   * ```
   * // creates a DeveryERC721Client with the default params
   * let deveryERC721Client = new DeveryERC721();
   *
   * //creates a deveryRegistryClient pointing to a custom address
   * let deveryERC721Client = new DeveryERC721({address:'0xf17f52151EbEF6C7334FAD080c5704DAAA16b732'});
   *
   * ```
   *
   * @param {ClientOptions} options network connection options
   *
   */
  constructor(options = {
    web3Instance: undefined,
    acc: undefined,
    address: undefined,
    walletPrivateKey: undefined,
    networkId: undefined,
    infuraProjectKey: undefined,
  }) {
    super(...arguments);

    options = Object.assign(
      {
        web3Instance: undefined,
        acc: undefined,
        address: undefined,
        walletPrivateKey: undefined,
        networkId: undefined,
      },
      options,
    );

    let address = options.address;
    let network = options.networkId;

    try {
      if (!options.web3Instance) {
        options.web3Instance = web3;
      }
      network = options.web3Instance.version.network;
      // console.log('it was not possible to find global web3');
    } catch (e) {
      // console.log('it was not possible to find global web3');
    }

    if (!network) {
      try {
        if (!options.web3Instance) {
          options.web3Instance = web3;
        }
        network = options.web3Instance.currentProvider.networkVersion;
      } catch (e) {
        // console.log('it was not possible to find global web3');
      }
    }

    if (!network) {
      network = options.networkId || 1;
    }

    if (!address) {
      address = deveryERC721Artifact.networks[network].address;
    }

    this.__deveryERC721Contract = new ethers.Contract(
      address,
      deveryERC721Artifact.abi,
      this.__signerOrProvider,
    );

    this.address = address;
    this.abi = deveryERC721Artifact.abi;
  }

  /**
   *   This method is used for claiming a product. The method returns you the transaction address of the claim.
   *   If the product have a maximum mintable quantity set and you try to claim a number of product bigger than the mintable products number set
   *   the method will not work
   *
   *   ***Usage example:***
   *
   *   ```
   *  // first you need to get a {@link DeveryERC721} instance
   *  let deveryErc721Client = new DeveryERC721();
   *  // now you can use it
   *
   * // Let's log the simplest case of use in the console
   *
   * deveryErc721Client.claimProduct('0x627306090abaB3A6e1400e9345bC60c78a8BEf57', 1).then(response =>
   *    console.log('response').catch(err => {
   *  // treat you error
   *  })
   * )
   *
   *   ```
   *
   *  for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   *
   * @param {string} productAddress address of the claimed product
   * @param {number} quantity quantity of claimed products
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones.
   */
  async claimProduct(productAddress, quantity = 1, overrideOptions = {}) {
    const result = await this.__deveryERC721Contract.claimProduct(
      productAddress,
      quantity,
      overrideOptions,
    );
    return result.valueOf();
  }

  /**
   * This method gives an estimation of how much gas will be used for the method {@link DeveryERC721#claimProduct|DeveryERC721.claimProduct}
   * the params that you pass to this method shall be exactly the same ones that you would pass to {@link DeveryERC721#claimProduct|DeveryERC721.claimProduct}.
   * the return of this method will be the total gas used to call {@link DeveryERC721#claimProduct|DeveryERC721.claimProduct} with the given parameters
   *
   * @param {string} productAddress address of the claimed product
   * @param {number} quantity quantity of claimed products
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones.
   *
   * @returns total gas used to call {@link DeveryERC721#claimProduct|DeveryERC721.claimProduct} with the given parameters
   */
  async estimateClaimProduct(
    productAddress,
    quantity = 1,
    overrideOptions = {},
  ) {
    const result = await this.__deveryERC721Contract.estimate.claimProduct(
      productAddress,
      quantity,
      overrideOptions,
    );
    return result.toNumber();
  }

  // x.__deveryERC721Contract.getApproved
  /**
   * This method returns you the total amount of approved transactions of the inspected product (referenced by it's token).
   *
   * ***Usage example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   * // Let's log the simplest case of use in the console
   *
   * deveryErc721Client.getApproved(address).then(response => console.log(`Number of approved transactions ${response}`))
   *
   * //response is going to be an hexadecimal number representing the amount of approved transactions made with it
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   *
   * @param {string} address token address to be inspected.
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones.
   */
  async getApproved(address, overrideOptions = {}) {
    return await this.__deveryERC721Contract.getApproved(
      address,
      overrideOptions,
    );
  }

  // x.__deveryERC721Contract.getProductsByOwner
  /**
   * Each brand has a blockchain address.
   * This function returns an array with all the product addresses owned by the address passed as a parameter.
   *
   * ***Usage example:***
   *
   * ```
   *  // first you need to get a {@link DeveryERC721} instance
   *  let deveryErc721Client = new DeveryERC721();
   *  // now you can use it
   *
   *  // Let's log the simplest case of use in the console.
   *
   *  deveryErc721Client.getProductsByOwner(addressOwner).then(response => console.log('these are the products owned by this address', response))
   *
   *  // Since this is a promise function you will need a .then statement to display the result
   *
   * ```
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {string} addressOwner the blockchain address of whom we want to know the owned tokens
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones
   */
  async getProductsByOwner(addressOwner, overrideOptions = {}) {
    const result = await this.__deveryERC721Contract.getProductsByOwner(
      addressOwner,
      overrideOptions,
    );
    return result.valueOf();
  }

  /**
   *
   * Listener for transfer approval events, this event triggers whenever a devery item is transferred in the blockchain.
   * Please note that ApprovalEventListeners do not stack, this means that whenever you set one you are
   * removing the last one. If you want to remove an ApprovalEventListener, just call this function passing undefined
   * as param.
   *
   * ***Usage example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   *
   *
   * deveryErc721Client.setApprovalEventListener((brandAccount,appAccount,active) => {
   *      // whenever an app created we will log it to the console
   *      console.log(`a brand has been updated ${brandAccount} - ${appAccount} ...`);
   * })
   *
   * // if you want to remove the listener you can simply pass undefined as parameter
   *
   * deveryErc721Client.setApprovalEventListener(undefined)
   *
   * // or that is equivalent to the above call
   *
   *
   *
   * deveryErc721Client.setApprovalEventListener()
   *
   *
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {ApprovalEventCallback} callback the callback that will be executed whenever an ApprovalForAll event is
   * triggered
   */
  setApprovalEventListener(callback) {
    const eventName = 'ApprovalForAll'; // @todo: check - may be it should be 'Approval'??
    this.__deveryERC721Contract.removeAllListeners(eventName);
    if (callback) {
      this.__deveryERC721Contract.on(eventName, callback);
    }
  }

  /**
   * This is a callback function that will be invoked in response to ApprovalEvents
   *
   *
   * @callback ApprovalEventCallback
   * @param {string} productAccount product account address.
   * @param {string} brandAccount brand account address.
   * @param {string} appAccount application account address.
   * @param {string} description description
   * @param {boolean} active
   *
   */

  /**
   *
   * Listener for transfer approval for all events, this event triggers whenever a devery item is transferred in the blockchain.
   * Please note that ApprovalForAllEventListeners do not stack, this means that whenever you set one you are
   * removing the last one. If you want to remove an ApprovalForAllEventListener, just call this function passing undefined
   * as param.
   *
   * ***Usage example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   *
   *
   * deveryErc721Client.setApprovalForAllEventListener((brandAccount,appAccount,active) => {
   *      // whenever an app created we will log it to the console
   *      console.log(`a brand has been updated ${brandAccount} - ${appAccount} ...`);
   * })
   *
   * // if you want to remove the listener you can simply pass undefined as parameter
   *
   * deveryRegistryClient.setApprovalForAllEventListener(undefined)
   *
   * // or that is equivalent to the above call
   *
   *
   *
   * deveryRegistryClient.setApprovalForAllEventListener()
   *
   *
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   *
   * @param {ApprovalEventCallback} callback the callback that will be executed whenever a Approval event is
   * triggered
   */
  setApprovalForAllEventListener(callback) {
    const eventName = 'Approval'; // @todo: check - may be it should be 'ApprovalAll'??
    this.__deveryERC721Contract.removeAllListeners(eventName);
    if (callback) {
      this.__deveryERC721Contract.on(eventName, callback);
    }
  }
  // @todo: re-check callback description
  /**
   *
   * Listener for transfer events, this event triggers whenever a devery item is transferred in the blockchain.
   * Please note that TransferEventListeners do not stack, this means that whenever you set one you are
   * removing the last one. If you want to remove a TransferEventListener, just call this function passing undefined
   * as param.
   *
   * ***Usage example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   *
   *
   * deveryErc721Client.setTransferEventListener((brandAccount,appAccount,active) => {
   *      // whenever an app created we will log it to the console
   *      console.log(`a brand has been updated ${brandAccount} - ${appAccount} ...`);
   * })
   *
   * // if you want to remove the listener you can simply pass undefined as parameter
   *
   * deveryErc721Client.setTransferEventListener(undefined)
   *
   * // or that is equivalent to the above call
   *
   *
   *
   * deveryErc721Client.setTransferEventListener()
   *
   *
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {ApprovalEventCallback} callback the callback that will be executed whenever a Transfer event is
   * triggered
   */
  setTransferEventListener(callback) {
    const eventName = 'Transfer';
    this.__deveryERC721Contract.removeAllListeners(eventName);
    if (callback) {
      this.__deveryERC721Contract.on(eventName, callback);
    }
  }

  /**
   *
   * Sets the maximum mintable quantity of a given token. *** If you don't set the maximum mintable quantity it will be infinite by default**
   *
   * @param {string} productAddress The address of the product which the mintable quantity will be set.
   * @param {string} quantity the allowed quantity of mintable products.
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones
   * @returns {Promise.<Transaction>} a promise that if resolved returns an transaction or raise an error in case of rejection.
   */
  async setMaximumMintableQuantity(
    productAddress,
    quantity,
    overrideOptions = {},
  ) {
    const result = await this.__deveryERC721Contract.setMaximumMintableQuantity(
      productAddress,
      quantity,
      overrideOptions,
    );
    return result.valueOf();
  }

  // x.__deveryERC721Contract.tokenIdToProduct
  /**
   * This method returns the blockchain address of a product, using its token as a parameter.
   *
   * ***Usage Example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // now you can use it
   *
   * // to use this function you need to have a token, which can be get through a function like tokenOfOwnerByIndex
   * // The token is a hexadecimal number
   *
   * deveryErc721Client.tokenIdToProduct(tokenId).then(response => console.log('this is your product address', response))
   *
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {string} tokenId The token of the product you wish to get the address of.
   * @return {Promise.<String>} a promise that if resolved returns a blockchain product address or raise an error in case of rejection.
   *
   */
  async tokenIdToProduct(tokenId) {
    const result = await this.__deveryERC721Contract.tokenIdToProduct(tokenId);
    return result.valueOf();
  }

  /**
   * This method returns the (hexadecimal) number of products owned by the account address passed as parameter to the function.
   *
   * ***Usage Example:***
   *
   *  ```
   * // first you need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // now you can use it
   *
   * // this method requires an owner address, which can be obtained on the metamask extension by clicking on your account name
   *
   *
   * deveryErc721Client.__deveryERC721Contract.balanceOf(ownerAddress).then(response => console.log('this account owns number of products: ', response))
   *
   * ```
   *
   * @param {string} ownerAddress Blockchain address of the inspected account.
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones.
   * @return {Promise.<*>} a promise that if resolved returns a hexadecimal number of products or raise an error in case of rejection.
   */
  async balanceOf(ownerAddress, overrideOptions = {}) {
    const result = await this.__deveryERC721Contract.balanceOf(
      ownerAddress,
      overrideOptions,
    );
    return result.toNumber();
  }

  /**
   * This method returns the token of a product by its index (it's a position in an array containing all the products owned by the owner address).
   *
   * ***Usage Example:***
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // now you can use it
   *
   * deveryErc721Client.__deveryERC721Contract.tokenOfOwnerByIndex(ownerAddress, index).then(response => ( console.log('product token', response)))
   *
   * // the product order is the same as the array returned by getProductByOwner() . Wherefore the index 0 it's the first address returned by the getProductByOwner method,
   * // the index 1 is the second address and so on.
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {string} ownerAddress blockchain address of that we want to know the owned tokens.
   * @param {number} index position of the product in the array of all products owned by the account corresponding to the address.
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones
   * @return {Promise.<*>} a promise that if resolved returns a token or raise an error in case of rejection.
   */
  async tokenOfOwnerByIndex(ownerAddress, index, overrideOptions = {}) {
    const result = await this.__deveryERC721Contract.tokenOfOwnerByIndex(
      ownerAddress,
      index,
      overrideOptions,
    );
    return result.toNumber();
  }

  // x.__deveryERC721Contract.totalAllowedProducts
  /**
   * This method allows you to query the number of tokens linked to the product address,
   * representing the amount of that product generated.
   * The response is returned as a hexadecimal number.
   *
   * ***Usage example:***
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   * deveryErc721Client.totalAllowedProducts(productAddress).then(response => {
   *     console.log(`this is the total amount of products Allowed ${response}`)
   * }).catch(err => {
   *  // handle exceptions here
   * })
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   * @param {string} productAddress Blockchain address of the product.
   * @return {Promise.<Number>} a promise that if resolved returns number of tokens or raise an error in case of rejection.
   */
  async totalAllowedProducts(productAddress) {
    const result = await this.__deveryERC721Contract.totalAllowedProducts(productAddress);
    return result.toNumber();
  }

  /**
   * You can generate a limited amount of products for each product address.
   * If you try to generated more products than you are allowed, this product
   * is going to be minted.
   * This method returns you the number of minted products for a specific product address.
   *
   * ***Usage Example:***
   *
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   * deveryErc721Client.totalMintedProducts(productAddress).then(response =>
   *  (console.log('you have the following number of minted products for this address ', response)))
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   *
   * @param {string} productAddress inspected product address.
   * @return {Promise.<Number>} a promise that if resolved returns number of minted products or raise an error in case of rejection.
   */
  async totalMintedProducts(productAddress) {
    const result = await this.__deveryERC721Contract.totalMintedProducts(productAddress);
    return result.toNumber();
  }

  /**
   * This method transfers the ownership of a claimed product item from one account to another.
   * The transfer must be made logged in the account referenced in the 'fromAddress' parameter,
   * otherwise the transfer will be denied.
   *
   * *** Usage Example: ***
   * ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * // now you can use it
   *
   * deveryErc721Client.safeTransferFrom(fromAddress, toAddress, tokenId).then(transaction => {
   *    console.log('your transaction was successful');
   *    // other stuff
   * }).catch(err => {
   *  if(err.message.indexOf('gas required exceeds allowance or always failing transaction') {
   *    console.log('You do not own the product you are trying to transfer')}
   * })
   *
   * ```
   *
   * for more info about how to get a {@link DeveryERC721|DeveryERC721 instance click here}.
   *
   *
   * @param {string} fromAddress blockchain address which the transfer is coming from
   * @param {string} toAddress blockchain address which the transfer is going to
   * @param {string} tokenId Token of the product item being transferred or blockchain address of the product account.
   * In case if product address was specified then for transfer is taken the first token (product item)
   * of the specified product owned by the fromAddress account.
   */
  async safeTransferFrom(
    fromAddress,
    toAddress,
    tokenId,
    overrideOptions = {},
  ) {
    if (!/^\d*$/.test(`${tokenId}`)) {
      tokenId = await this.getTokenIdByAddress(tokenId, fromAddress);
    }
    const result = await this.__deveryERC721Contract[
      'safeTransferFrom(address,address,uint256)'
    ](fromAddress, toAddress, tokenId, overrideOptions);
    return result.valueOf();
  }

  async estimateSafeTransferFrom(
    fromAddress,
    toAddress,
    tokenId,
    overrideOptions = {},
  ) {
    if (!/^\d*$/.test(`${tokenId}`)) {
      tokenId = await this.getTokenIdByAddress(tokenId, fromAddress);
    }
    const result = await this.__deveryERC721Contract.estimate[
      'safeTransferFrom(address,address,uint256)'
    ](fromAddress, toAddress, tokenId, overrideOptions);
    return result.toNumber();
  }

  async getTokenIdByAddress(address, wallet) {
    const balance = await this.balanceOf(wallet);
    address = address.toLocaleLowerCase();
    for (let i = 0; i < balance; i++) {
      // TODO: optimize
      const tokenId = await this.tokenOfOwnerByIndex(wallet, i);
      const prodAddress = await this.tokenIdToProduct(tokenId);
      if (prodAddress.toLocaleLowerCase() === address) {
        return tokenId;
      }
    }
    throw new Error('token id not found');
  }

  /**
   *  This method creates a devery registry address for the desired contract,
   *  so the user is able to properly use the devery ERC721 methods.
   *
   *
   *  ***Usage example:***
   *
   *  ```
   * // first you need to get a {@link DeveryERC721} instance
   * let deveryErc721Client = new DeveryERC721();
   * //n ow you can use it
   *
   * // then you have to pass the devery contract method passing the contract as a parameter
   * deveryErc721Client.setDeveryRegistryAddress(address).then(transaction => {
   *    console.log(transaction) }).catch(err => {
   *        // treat your errors here
   *    })
   *  ```
   * @param {string} deveryRegistryContractAddress address of the deployed contract.
   * @param {TransactionOptions} [overrideOptions] gas options to override the default ones.
   */
  async setDeveryRegistryAddress(
    deveryRegistryContractAddress,
    overrideOptions = {},
  ) {
    const result = await this.__deveryERC721Contract.setDeveryRegistryAddress(
      deveryRegistryContractAddress,
      overrideOptions,
    );
    return result.valueOf();
  }

  /**
   * This method returns you whether the account owns tokens of the specified product or not.
   * The return of this function is a boolean value.
   *
   * ***Usage Example***
   * ```
   * // First you'll need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // Then you will need to pass an account address and a product address as parameters
   * deveryErc721Client.hasAccountClaimendProduct(ownerAddress, productAddress)
   *  .then(hasProduct => console.log(hasProduct))
   *  .catch(err => {
   *    //treat errors
   *  })
   * ```
   *
   * @param {string} ownerAddress Blockchain address of the inspect account
   * @param {string} productAddress Blockchain address of the checked product
   */
  async hasAccountClaimendProduct(ownerAddress, productAddress) {
    const ownedProducts = await this.getProductsByOwner(ownerAddress);
    return ownedProducts.includes(productAddress);
  }

  /**
   * This methods sets the metadata URI for a given token, this URI can be any valid URI in the internet and is expected to contain a metadata file
   * with info about the token. this file is expected to be a valid json file. bellow you can see an example of such file
   *
   * IMPORTANT: only the address that owns a NFT can set it's metadata URI
   *
   * it's important to remember that it's client`s responsability to host and ensure that the URI points to a valid metadata file
   *
   * ```
   * {
   *   "name": "Imaginary product",
   *   "description": "This describes our imaginary product and how it behaves",
   *   "image": "https://markery.io/img/markery-io-logo.svg",
   *  }```
   *
   *  this is not an extensive example and you can add more data to suite your own use cases
   *
   *
   * ***Usage Example***
   * ```
   * // First you'll need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // then you have to pass the devery contract method passing the contract as a parameter
   * deveryErc721Client.setTokenURI(tokenId,address).then(transaction => {
   *    console.log(transaction) }).catch(err => {
   *        // treat your errors here
   *    })
   *  ```
   * ```
   *
   * @param {string} tokenId Token for which the uri will be set
   * @param {string} tokenUri URI for the token
   */
  async setTokenURI(tokenId, tokenUri, overrideOptions = {}) {
    const result = await this.__deveryERC721Contract.setTokenUri(
      tokenId,
      tokenUri,
      overrideOptions,
    );
    return result.valueOf();
  }

  /**
   * returns the URI containing metadata for a given token, if there's no such URI for the given token an empty string will be returned
   *
   *
   * ***Usage Example***
   * ```
   * // First you'll need to get a {@link DeveryERC721} instance
   *
   * let deveryErc721Client = new DeveryERC721();
   *
   * // then you have to pass the devery contract method passing the contract as a parameter
   * deveryErc721Client.tokenURI(tokenId).then(address => {
   *    console.log(address) }).catch(err => {
   *        // treat your errors here
   *    })
   *  ```
   * ```
   *
   * @param {string} tokenId Token for which the uri will be retrieved
   */
  async tokenURI(tokenId) {
    const uri = await this.__deveryERC721Contract.tokenUri(tokenId);
    return uri;
  }
}

export default DeveryERC721;