Source

modules/TransactionManager.ts

import Web3 from "web3";
import { VaultForMintableERC721 } from "../..";
import { IMyriaClient } from "../clients/MyriaClient";
import { AssetMarketpAPI, CommonAPI } from "../core/apis";
import { TransactionAPI } from "../core/apis/transaction.api";
import { TokenType, VaultForERC20, VaultForETH } from "../types";
import { APIResponseType } from "../types/APIResponseType";
import {
  BulkTransferTokenRequestAPIParams,
  BulkTransferTokenResponse,
  TransactionPagingDetails,
  ItemSignableTransferParams,
  SignableBulkTransferParams,
  SignableBulkTransferResponse,
  TransactionCompleteParams,
  TransactionCompleteResponse,
  TransactionData,
  TransferAPIInput,
  TransferCommonParams,
  TransferERC20Params,
  TransferERC721Params,
  TransferResponse,
  TransferTokenParams,
  TransactionPagingData,
  ItemSignableBurnParams,
  BurnTokenParams,
  BurnTokenResponse,
  BurnTokensRequestAPIParams,
  SignableBurnTokensParams,
  WhitelistTokensResponse,
  TransferETHParams,
  ICommonError,
} from "../types/TransactionTypes";
import { CommonModule } from "./CommonModule";

const QUANTUM_ERC20 = "10000000000";

/**
 * Create TransactionManager module
 * @class TransactionManager
 * @param {IMyriaClient} IMyriaClient Interface of Myria Client
 * @example <caption>TransactionManage instance.</caption>
 * // + Approach 1:
  const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
  };

  const moduleFactory = ModuleFactory.getInstance(mClient);
  const transactionManager = moduleFactory.getTransactionManager();

  // + Approach 2:
  const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
  };
  const transactionManager = new TransactionManager(mClient);

 */
export class TransactionManager {
  private transactionAPI: TransactionAPI;
  private assetMarketplaceAPI: AssetMarketpAPI;
  private commonModule: CommonModule;
  private commonAPI: CommonAPI;

  constructor(mClient: IMyriaClient) {
    this.transactionAPI = new TransactionAPI(mClient.env);
    this.commonModule = new CommonModule(mClient);
    this.commonAPI = new CommonAPI(mClient.env);
    this.assetMarketplaceAPI = new AssetMarketpAPI(mClient.env);
  }

  /**
   * @summary Get transaction history list by stark key with paging options. This is designed for the purposes of small history reports sorted by latest date.
   * @description Paging can be shown one after another (you cannot request for page 3 without first seeing page 1 and 2, for example, as you need details of last transaction of previous page to query for next page).
   * @param {TransactionPagingDetails} TransactionPagingDetails Request params to query the list of transactions
   * @returns {TransactionData[]} List of transaction details
   */
  public async getTransactionList(
    payload: TransactionPagingDetails
  ): Promise<any> {
    const transactionList = await this.transactionAPI.getTransactionList(
      payload
    );
    return transactionList;
  }

  /**
   * @summary Get transaction details by transaction ID
   * @param {number} transactionId Unique ID of transaction
   * @returns {TransactionData} Transaction details information (including transactionStatus and createdAt, updatedAt...)
   * @throws {string} Exception: Transaction ID should be valided and greater than 0
   * @throws {string} Exception: Get transaction details failed with internal server error
   */
  public async getTransactionDetails(
    transactionId: number
  ): Promise<TransactionData> {
    if (!transactionId || transactionId === 0) {
      throw new Error("Transaction ID should be valided and greater than 0");
    }

    const transactionResponseData =
      await this.transactionAPI.getTransactionDetails(transactionId);
    if (transactionResponseData.status !== "success") {
      throw new Error(
        "Get transaction details failed with internal server error"
      );
    }

    return transactionResponseData.data;
  }

  /**
   * @summary Single Transfer ERC-721 (MINTABLE NFT) Token
   * + The function is just supported for MINTABLE_ERC721 only
   * @description After transfer was triggered, we can query the status of the transaction with the following functions:
   * + getTransactionDetails(transactionId: number) {return TransactionData}
   * + getTransactionsByPartnerRefID(partnerRefID: string) {return []TransactionData}
   * @param {TransferERC721Params} transferParams Transfer ERC-721 token params (including sender and receiver's information)
   * @throws {string} Exception: Sender vault is not found
   * @throws {string} Exception: Receiver vault is not found
   * @throws {string} Http Status 400 - Sender/Receiver vaults does not exist
   * @throws {string} Http Status 400 - Signature is invalid
   * @throws {string} Http Status 400 - Vault IDs does not have enough funds
   * @returns {TransferResponse} Transaction details (such as transactionID, transactionStatus...)
   */
  public async transferERC721Token(
    transferParams: TransferERC721Params
  ): Promise<TransferResponse> {
    const requestSenderVaultERC721: VaultForMintableERC721 = {
      starkKey: transferParams.senderPublicKey,
      tokenId: transferParams.tokenId,
      tokenAddress: transferParams.tokenAddress,
    };

    const requestReceiverVaultERC721: VaultForMintableERC721 = {
      starkKey: transferParams.receiverPublicKey,
      tokenId: transferParams.tokenId,
      tokenAddress: transferParams.tokenAddress,
    };
    const senderVault = await this.commonAPI.createVaultForMintableERC721(
      requestSenderVaultERC721
    );

    if (senderVault.status !== "success") {
      const error: ICommonError = {
        message: "Retrieve vaults ERC721 for sender has failed",
        code: "",
        error: requestReceiverVaultERC721
      }
      throw Error(JSON.stringify(error));
    }

    const receiverVault = await this.commonAPI.createVaultForMintableERC721(
      requestReceiverVaultERC721
    );
    if (receiverVault.status !== "success") {
      const error: ICommonError = {
        message: "Retrieve vaults ERC721 for receiver has failed",
        code: "",
        error: JSON.stringify(requestReceiverVaultERC721)
      }
      throw Error(JSON.stringify(error));
    }

    const transferCommonParams: TransferCommonParams = {
      senderVaultId: senderVault.data?.vaultId,
      senderPublicKey: transferParams.senderPublicKey,
      senderWalletAddress: transferParams.senderWalletAddress,
      receiverVaultId: receiverVault.data?.vaultId,
      receiverPublicKey: transferParams.receiverPublicKey,
      assetId: senderVault.data?.assetId,
      quantizedAmount: transferParams.quantizedAmount,
      tokenType: TokenType.MINTABLE_ERC721,
      groupRequestId: transferParams.groupRequestId,
      partnerRefId: transferParams.partnerRefId,
      myriaPrivateStarkKey: transferParams.myriaPrivateKey,
      path: "/v1/transactions/transfer",
    };

    const transferResult = await this.transferTokenCommon(transferCommonParams);
    return transferResult;
  }

  /**
   * @summary Asynchronous bulk transfer for NFT Tokens (such as: ERC-721 Tokens, Marketplace NFTs) 
   * - Function only supports MINTABLE_ERC721 and NFTs which are minted on Myria System
   * @param {TransferTokenParams} transferTokenParams Data regarding sender and receivers relevant for the transfer.
   * @description After bulk transfer was triggered, we can query the status of the batch with the following functions:
   * + getTransactionsByGroupRequestIDAndPartnerRefID(groupRequestID: string, partnerRefID: string)
   * + getTransactionsByPartnerRefID(partnerRefID: string)
   * @returns {BulkTransferTokenResponse} Transaction data list which have been captured and validated 
   * - Response structure consist of 3 group of transactions failed[] / success[] / validationFailed[]
   * - All transactions in failed[], relate to failures due to not enough funds or other internal server errors. These transactions cannot be processed.
   * - All transactions in validationFailed[], relate to failures due to validation such as L2's signature. These can be retried with amended data.
   * - All transactions in success[], indicate that they have been recorded and will be processed by the system.
   * @throws {string} Exception: Sender wallet address is required
   * @throws {string} Exception: Bulk transfer should include at least one transfer
   * @throws {string} Exception: Receiver wallet address is required
   * @throws {string} Exception: Only MINTABLE_ERC-721 tokens are valid for this type of bulk transfer
   * @throws {string} Exception: Token address is required
   * @throws {string} Error code 409 - Request-ID/Group-Request-ID is already exists
   * @throws {string} Http error code 400 - User wallet (sender or receiver) is not registered
   * @throws {string} Http error code 400 - Vault ID does not have enough funds
   * @throws {string} Http error code 400 - Signature is invalid
   * @example <caption>Sample code on Testnet (Staging) env</caption> 
   * 
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const YOUR_NFT_CONTRACT_ADDRESS = "0xA06116D9...."; 
    const RECEIVER_WALLET_ADDRESS = '0xd0D8A467E....'; // Your receiver/users wallet address
    const SENDER_WALLET_ADDRESS = '0x724f337bF0F....'; // Must be the owner of tokens, sender wallet address

    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();

    const transferredItems: ItemSignableTransferParams[] = [
      {
        quantizedAmount: 1, // Should be 1 as always
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '1' // Your minted token ID
        },
      },
      {
        quantizedAmount: 1,
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '2' // Your minted token ID
        },
      },
      {
        quantizedAmount: 1,
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '3' // Your minted token ID
        },
      },
    ];

    const transferTokenParams: TransferTokenParams = {
      senderWalletAddress: SENDER_WALLET_ADDRESS,
      groupRequestId: '7257d29c-c96a-4302-8eaf-368a0d62b977', // Can use random UUID to generate groupRequestID
      requestId: '7257d29c-c96a-4302-8eaf-368a0d62b977',  // Can use random UUID to generate requestID
      partnerRefId: 'Project-ID', // Project-ID on Myria System
      description: 'Test-Test Bulk Transfer',
      items: transferredItems,
    };

    const transferResult = await transactionManager.bulkTransferNfts(
      transferTokenParams,
    );
   * 
   */
  public async bulkTransferNfts(
    transferTokenParams: TransferTokenParams
  ): Promise<APIResponseType<BulkTransferTokenResponse>> {
    if (!transferTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (!transferTokenParams.items || transferTokenParams.items.length === 0) {
      throw new Error("Bulk transfer should include at least one transfer.");
    }

    const transferredItems: ItemSignableTransferParams[] = [];

    transferTokenParams.items.forEach((item) => {
      if (!item.receiverWalletAddress) {
        throw new Error("Receiver wallet address is required");
      }
      if (item.tokenType !== (TokenType.ERC721 && TokenType.MINTABLE_ERC721)) {
        throw new Error(
          "Only MINTABLE_ERC-721 tokens are valid for this type of bulk transfer"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }
      if (!item.tokenData?.tokenId) {
        throw new Error("Token ID is required for transfer ERC-721 tokens ");
      }
      const transferredItem: ItemSignableTransferParams = {
        quantizedAmount: item.quantizedAmount,
        receiverWalletAddress: item.receiverWalletAddress,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      transferredItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBulkTransferredParams: SignableBulkTransferParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      items: transferredItems,
    };

    const fullPayloadTransferData =
      await this.transactionAPI.signableBulkTransfer(
        signableBulkTransferredParams
      );

    if (!fullPayloadTransferData) {
      throw new Error("Transfer payload data is required");
    }

    // Create generateFullPayloadForBulkTransfer list of signature
    const fullPayloadTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        transferTokenParams.senderWalletAddress,
        TokenType.MINTABLE_ERC721,
        fullPayloadTransferData,
        transferTokenParams.myriaPrivateKey
      );
    console.log("Full transfer payload -> ", fullPayloadTransferred);

    // Trigger transfer
    const bulkTransferRequestApi: BulkTransferTokenRequestAPIParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      requestId: transferTokenParams.requestId,
      groupRequestId: transferTokenParams.groupRequestId,
      partnerRefId: transferTokenParams.partnerRefId,
      description: transferTokenParams.description,
      items: fullPayloadTransferred,
      isWaitingForValidation: transferTokenParams.isWaitingForValidation
    };

    const bulkTransferResult =
      await this.assetMarketplaceAPI.bulkTransferERC721Token(
        bulkTransferRequestApi
      );
    console.log(
      "Bulk Transfer Response Nft API -> ",
      JSON.stringify(bulkTransferResult)
    );
    return bulkTransferResult;
  }

  public async bulkTransferNftsV2(
    transferTokenParams: TransferTokenParams
  ): Promise<APIResponseType<BulkTransferTokenResponse>> {
    if (!transferTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (!transferTokenParams.items || transferTokenParams.items.length === 0) {
      throw new Error("Bulk transfer should include at least one transfer.");
    }

    const transferredItems: ItemSignableTransferParams[] = [];

    transferTokenParams.items.forEach((item) => {
      if (!item.receiverWalletAddress) {
        throw new Error("Receiver wallet address is required");
      }
      if (item.tokenType !== (TokenType.ERC721 && TokenType.MINTABLE_ERC721)) {
        throw new Error(
          "Only MINTABLE_ERC-721 tokens are valid for this type of bulk transfer"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }
      if (!item.tokenData?.tokenId) {
        throw new Error("Token ID is required for transfer ERC-721 tokens ");
      }
      const transferredItem: ItemSignableTransferParams = {
        quantizedAmount: item.quantizedAmount,
        receiverWalletAddress: item.receiverWalletAddress,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      transferredItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBulkTransferredParams: SignableBulkTransferParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      items: transferredItems,
    };

    const fullPayloadTransferData =
      await this.transactionAPI.signableBulkTransfer(
        signableBulkTransferredParams
      );

    if (!fullPayloadTransferData) {
      throw new Error("Transfer payload data is required");
    }

    // Create generateFullPayloadForBulkTransfer list of signature
    const fullPayloadTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        transferTokenParams.senderWalletAddress,
        TokenType.MINTABLE_ERC721,
        fullPayloadTransferData,
        transferTokenParams.myriaPrivateKey
      );
    console.log("Full transfer payload -> ", fullPayloadTransferred);

    // Trigger transfer
    const bulkTransferRequestApi: BulkTransferTokenRequestAPIParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      requestId: transferTokenParams.requestId,
      groupRequestId: transferTokenParams.groupRequestId,
      partnerRefId: transferTokenParams.partnerRefId,
      description: transferTokenParams.description,
      items: fullPayloadTransferred,
      isWaitingForValidation: transferTokenParams.isWaitingForValidation
    };

    const bulkTransferResult =
      await this.assetMarketplaceAPI.bulkTransferNfts(
        bulkTransferRequestApi
      );
    console.log(
      "Bulk Transfer Response Nft API -> ",
      JSON.stringify(bulkTransferResult)
    );
    return bulkTransferResult;
  }

  public async getSignableDetailsTransferERC20(
    transferTokenParams: TransferTokenParams
  ): Promise<BulkTransferTokenRequestAPIParams> {
    if (!transferTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (!transferTokenParams.items || transferTokenParams.items.length === 0) {
      throw new Error("Bulk transfer should include at least one transfer");
    }

    const transferredItems: ItemSignableTransferParams[] = [];

    transferTokenParams.items.forEach((item) => {
      if (!item.receiverWalletAddress) {
        throw new Error("Receiver wallet address is required");
      }
      if (
        item.tokenType !== TokenType.ERC20 &&
        item.tokenType !== TokenType.MINTABLE_ERC20
      ) {
        throw new Error(
          "Only ERC20 Tokens are valid for this type of bulk transfer"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }
      const transferredItem: ItemSignableTransferParams = {
        quantizedAmount: item.quantizedAmount,
        receiverWalletAddress: item.receiverWalletAddress,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      transferredItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBulkTransferredParams: SignableBulkTransferParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      items: transferredItems,
    };

    console.time("Signable_Transfer_ERC20");
    const fullPayloadTransferData =
      await this.transactionAPI.signableBulkTransfer(
        signableBulkTransferredParams
      );
    console.timeEnd("Signable_Transfer_ERC20");

    // console.log("Full signable transfer payload", fullPayloadTransferData);
    if (!fullPayloadTransferData) {
      throw new Error("Transfer payload data is required");
    }

    // Create generateFullPayloadForBulkTransfer list of signature
    console.time("Generate_FullPayloadTransfer_ERC20");
    const fullPayloadTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        transferTokenParams.senderWalletAddress,
        TokenType.ERC20,
        fullPayloadTransferData,
        transferTokenParams.myriaPrivateKey
      );
    console.timeEnd("Generate_FullPayloadTransfer_ERC20");

    // Trigger transfer
    const payloadTransferDetails: BulkTransferTokenRequestAPIParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      requestId: transferTokenParams.requestId,
      groupRequestId: transferTokenParams.groupRequestId,
      partnerRefId: transferTokenParams.partnerRefId,
      description: transferTokenParams.description,
      items: fullPayloadTransferred,
      isWaitingForValidation: transferTokenParams.isWaitingForValidation,
    };

    return payloadTransferDetails;
  }

  /**
   * @summary Async bulk transfer for ERC-20 Tokens (such as: Myria Tokens, ...)
   * - Function only supports ERC20 and Myria Tokens (ERC20) which are registered in Myria System already (i.e. via a deposit).
   * @param {TransferTokenParams} transferTokenParams Data regarding sender and receivers relevant for the transfer.
   * @description After bulk transfer was triggered, we can query the status of the batch with the following functions:
   * + getTransactionsByGroupRequestIDAndPartnerRefID(groupRequestID: string, partnerRefID: string)
   * + getTransactionsByPartnerRefID(partnerRefID: string)
   * @example <caption>Sample code on Testnet (Staging) env</caption> 
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const MYR_TOKEN_ADDRESS_EXAMPLE = "0xA06116D9...."; // ERC-20 token address - and make sure it is registered in Myria System already
    const RECEIVER_WALLET_ADDRESS = '0xd0D8A467E....'; // Your receiver/users wallet address
    const SENDER_WALLET_ADDRESS = '0x724f337bF0F....'; // Must be the owner of tokens, sender wallet address

    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();

    const transferredItems: ItemSignableTransferParams[] = [
      {
        quantizedAmount: String(convertAmountToQuantizedAmount(1)),
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: MYR_TOKEN_ADDRESS_EXAMPLE,
        },
      },
      {
        quantizedAmount: String(convertAmountToQuantizedAmount(2)),
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: MYR_TOKEN_ADDRESS_EXAMPLE,
        },
      },
      {
        quantizedAmount: String(convertAmountToQuantizedAmount(3)),
        receiverWalletAddress: RECEIVER_WALLET_ADDRESS,
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: MYR_TOKEN_ADDRESS_EXAMPLE,
        },
      },
    ];

    const transferTokenParams: TransferTokenParams = {
      senderWalletAddress: SENDER_WALLET_ADDRESS,
      groupRequestId: '7257d29c-c96a-4302-8eaf-368a0d62b977', // Can use random UUID to generate groupRequestID
      requestId: '7257d29c-c96a-4302-8eaf-368a0d62b977',  // Can use random UUID to generate requestID
      partnerRefId: 'Project-ID', // Partner project ID
      description: 'Test-Test Bulk Transfer',
      items: transferredItems,
    };

    const transferResult = await transactionManager.bulkTransferERC20Token(
      transferTokenParams,
    );
   * @returns {BulkTransferTokenResponse} Transaction data list which have been captured and validated 
   * - Response structure consist of 3 group of transactions: failed[] / success[] / validationFailed[]
   * - All transactions in failed[], relate to failures due to not enough funds or other internal server errors. These transactions cannot be processed.
   * - All transactions in validationFailed[], relate to failures due to validation such as L2's signature. These can be retried with amended data.
   * - All transactions in success[], indicate that they have been recorded and will be processed by the system.
   * @throws {string} Exception: Sender wallet address is required
   * @throws {string} Exception: Bulk transfer should include at least one transfer
   * @throws {string} Exception: Receiver wallet address is required
   * @throws {string} Exception: Only ERC20 Tokens are valid for this type of bulk transfer
   * @throws {string} Exception: Token address is required
   * @throws {string} Http error code 409 - Request-ID/Group-Request-ID already exists
   * @throws {string} Http error code 400 - User wallet (sender or receiver) is not registered
   * @throws {string} Http error code 400 - Vault ID does not have enough funds
   * @throws {string} Http error code 400 - Signature is invalid
   */
  public async bulkTransferERC20Token(
    transferTokenParams: TransferTokenParams
  ): Promise<APIResponseType<BulkTransferTokenResponse>> {
    if (!transferTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (!transferTokenParams.items || transferTokenParams.items.length === 0) {
      throw new Error("Bulk transfer should include at least one transfer");
    }

    const transferredItems: ItemSignableTransferParams[] = [];

    transferTokenParams.items.forEach((item) => {
      if (!item.receiverWalletAddress) {
        throw new Error("Receiver wallet address is required");
      }
      if (
        item.tokenType !== TokenType.ERC20 &&
        item.tokenType !== TokenType.MINTABLE_ERC20
      ) {
        throw new Error(
          "Only ERC20 Tokens are valid for this type of bulk transfer"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }
      const transferredItem: ItemSignableTransferParams = {
        quantizedAmount: item.quantizedAmount,
        receiverWalletAddress: item.receiverWalletAddress,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      transferredItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBulkTransferredParams: SignableBulkTransferParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      items: transferredItems,
    };

    console.time("Signable_Transfer_ERC20");
    const fullPayloadTransferData =
      await this.transactionAPI.signableBulkTransfer(
        signableBulkTransferredParams
      );
    console.timeEnd("Signable_Transfer_ERC20");

    // console.log("Full signable transfer payload", fullPayloadTransferData);
    if (!fullPayloadTransferData) {
      throw new Error("Transfer payload data is required");
    }

    // Create generateFullPayloadForBulkTransfer list of signature
    console.time("Generate_FullPayloadTransfer_ERC20");
    const fullPayloadTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        transferTokenParams.senderWalletAddress,
        TokenType.ERC20,
        fullPayloadTransferData,
        transferTokenParams.myriaPrivateKey
      );
    console.timeEnd("Generate_FullPayloadTransfer_ERC20");

    // Trigger transfer
    const bulkTransferRequestApi: BulkTransferTokenRequestAPIParams = {
      senderWalletAddress: transferTokenParams.senderWalletAddress,
      requestId: transferTokenParams.requestId,
      groupRequestId: transferTokenParams.groupRequestId,
      partnerRefId: transferTokenParams.partnerRefId,
      description: transferTokenParams.description,
      items: fullPayloadTransferred,
      isWaitingForValidation: transferTokenParams.isWaitingForValidation,
    };

    console.time("BulkTransfer_ERC20");
    const bulkTransferResult = await this.transactionAPI.bulkTransferERC20Token(
      bulkTransferRequestApi
    );
    console.timeEnd("BulkTransfer_ERC20");
    console.log(
      "Bulk Transfer Response ERC-20 API -> ",
      JSON.stringify(bulkTransferResult)
    );
    return bulkTransferResult;
  }

  // /**
  //  * @description Query the list of the transaction based on request ID
  //  * @param {string} requestID The unique request ID of the transaction
  //  * @throws {string} Exception: RequestID is required
  //  * @returns {TransactionData[]} Transaction data list (such as transactionID, transactionStatus...)
  //  * @example <caption>Sample code on Testnet (Staging) env</caption>
  //   const mClient: IMyriaClient = {
  //     networkId: Network.SEPOLIA,
  //     provider: web3Instance.currentProvider,
  //     web3: web3Instance,
  //     env: EnvTypes.STAGING,
  //   };

  //   const requestID = "923a78a2-49a2-4eb9-8163-20dddd524a8c";
  //   const moduleFactory = ModuleFactory.getInstance(mClient);
  //   const transactionManager = moduleFactory.getTransactionManager();
  //   const result = await transactionManager.getTransactionsByRequestID(requestID);

  //   console.log('Transaction result -> ', result);
  //  */
  public async getTransactionsByRequestID(
    requestID: string
  ): Promise<APIResponseType<TransactionData[]>> {
    if (!requestID) {
      throw new Error("RequestID is required");
    }

    return this.transactionAPI.getTransactionsByRequestID(requestID);
  }

  /**
   * @description Query a list of transactions based on group request ID and Partner Ref ID
   * @param {string} groupReqID The unique group request ID of the transaction batch
   * @param {string} partnerRefID The unique partner reference ID as Project ID
   * @param {TransactionPagingDetails=} transactionPaging The pagination params (which included starkKey, limit, createdAt, transactionCategory for query next page)
   * @throws {string} Exception: Partner Reference ID is required
   * @throws {string} Exception: Group Request ID is required
   * @returns {TransactionData[]} List of transactions data which indicate the status of batch, transaction results, transaction details information
   * (such as transactionID, transactionStatus...)
   * @example <caption>Sample code on Testnet (Staging) env</caption>
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const groupRequestID = "e2fb1ef6-680b-4515-9ca6-0c46bc026ecd";
    const partnerRefId = "10"; // Unique ItemSignableTransferParamsProject ID in Myria System
    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();
    const result = await transactionManager.getTransactionsByGroupRequestIDAndPartnerRefID(
      groupRequestID,
      partnerRefId
    );

    console.log('Transaction result -> ', result);
   */
  public async getTransactionsByGroupRequestIDAndPartnerRefID(
    groupReqID: string,
    partnerRefID: string,
    transactionPaging?: TransactionPagingDetails
  ): Promise<APIResponseType<TransactionPagingData>> {
    if (!partnerRefID) {
      throw new Error("Partner Reference ID is required");
    }

    if (!groupReqID) {
      throw new Error("Group Request ID is required");
    }

    return this.transactionAPI.getTransactionsByGroupReqIDAndPartnerRefID(
      groupReqID,
      partnerRefID,
      transactionPaging
    );
  }

  /**
   * @description Query a list of transactions based on partner reference ID (which should be project ID)
   * @param partnerRefID The unique partner reference ID (Project ID)
   * @throws {string} Exception: Partner reference ID is required
   * @returns {TransactionData[]} List of transactions data which indicate the status of batch, transaction results, transaction details information
   * (such as transactionID, transactionStatus...)
   * @example <caption>Sample code on Testnet (Staging) env</caption>
    
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const partnerRefId = "Project-ID"; // Unique ID of the project on Myria System
    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();
    const result = await transactionManager.getTransactionsByPartnerRefID(
      partnerRefId
    );

    console.log('Transaction result -> ', result);
   */
  public async getTransactionsByPartnerRefID(
    partnerRefID: string
  ): Promise<APIResponseType<TransactionData[]>> {
    if (!partnerRefID) {
      throw new Error("Partner reference ID");
    }

    return this.transactionAPI.getTransactionsByPartnerRefID(partnerRefID);
  }

  public async signableBulkTransferDetail(
    params: SignableBulkTransferParams
  ): Promise<SignableBulkTransferResponse> {
    if (!params) {
      throw new Error("Transfer params is required");
    }
    if (!params?.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (
      params.senderWalletAddress &&
      !Web3.utils.isAddress(params.senderWalletAddress)
    ) {
      throw new Error("Sender Wallet Address is Invalid");
    }

    if (params?.items.length === 0) {
      throw new Error("Transfer params items is required");
    } else {
      params.items.forEach((paramItemValue: ItemSignableTransferParams) => {
        if (!paramItemValue.receiverWalletAddress) {
          throw new Error("Receive wallet address is required");
        }
        if (
          paramItemValue.receiverWalletAddress &&
          !Web3.utils.isAddress(paramItemValue.receiverWalletAddress)
        ) {
          throw new Error("Receive wallet address is Invalid");
        }
        if (!paramItemValue.tokenType) {
          throw new Error("Token type is required");
        }
        if (!paramItemValue.quantizedAmount) {
          throw new Error("Quantized amount is required");
        }
        if (paramItemValue.tokenType) {
          switch (paramItemValue.tokenType) {
            case TokenType.MINTABLE_ERC20:
            case TokenType.ERC20:
              if (!paramItemValue.tokenData?.tokenAddress) {
                throw new Error("Token Address is required");
              }
              break;
            case TokenType.MINTABLE_ERC721:
            case TokenType.ERC721:
              if (
                !paramItemValue.tokenData?.tokenAddress ||
                !paramItemValue.tokenData?.tokenId
              ) {
                throw new Error("Token Address and Token Id are required");
              }
              break;
            default:
              break;
          }
        }
      });
    }

    try {
      const result = await this.transactionAPI.signableBulkTransfer(params);
      return result;
    } catch (err) {
      throw new Error(`Bulk Transfer failed: ${err}`);
    }
  }

  /**
   * @summary Single Transfer ERC-20 Token
   * @param {TransferERC20Params} transferParams Transfer ERC-20 Tokens params (include information for Sender/Receiver)
   * @description After transfer was triggered, we can query the status of the transaction with the following functions:
   * + getTransactionDetails(transactionId: number) {return TransactionData}
   * + getTransactionsByPartnerRefID(partnerRefID: string) {return []TransactionData}
   * @returns {TransferResponse} Transactions data which indicate the status of batch, transaction results, transaction details information
   * (such as transactionID, transactionStatus...)
   * @throws {string} Exception: Sender vault is not found
   * @throws {string} Exception: Receiver vault is not found
   * @throws {string} Http Status 400 - Sender/Receiver vaults do not exist
   * @throws {string} Http Status 400 - Signature is invalid
   * @throws {string} Http Status 400 - Vault IDs does not have enough funds
   */
  public async transferERC20Token(
    transferParams: TransferERC20Params
  ): Promise<TransferResponse> {
    const requestSenderVaultERC20: VaultForERC20 = {
      starkKey: transferParams.senderPublicKey,
      tokenAddress: transferParams.tokenAddress,
      quantum: QUANTUM_ERC20,
    };

    const requestReceiverVaultERC20: VaultForERC20 = {
      starkKey: transferParams.receiverPublicKey,
      tokenAddress: transferParams.tokenAddress,
      quantum: QUANTUM_ERC20,
    };
    const senderVault = await this.commonAPI.retrieveERC20Vault(
      requestSenderVaultERC20
    );

    if (senderVault.status !== "success") {
      throw Error(
        '"Request sender vault failed => ' +
          JSON.stringify(requestSenderVaultERC20)
      );
    }

    const receiverVault = await this.commonAPI.retrieveERC20Vault(
      requestReceiverVaultERC20
    );
    if (receiverVault.status !== "success") {
      throw Error(
        '"Request receiver vault failed => ' + JSON.stringify(receiverVault)
      );
    }

    const transferCommonParams: TransferCommonParams = {
      senderVaultId: senderVault.data?.vaultId,
      senderPublicKey: transferParams.senderPublicKey,
      senderWalletAddress: transferParams.senderWalletAddress,
      receiverVaultId: receiverVault.data?.vaultId,
      receiverPublicKey: transferParams.receiverPublicKey,
      assetId: senderVault.data?.assetId,
      quantizedAmount: transferParams.quantizedAmount,
      tokenType: TokenType.ERC20,
      groupRequestId: transferParams.groupRequestId,
      partnerRefId: transferParams.partnerRefId,
      myriaPrivateStarkKey: transferParams.myriaPrivateKey,
      path: "/v1/transactions/transfer",
    };

    const transferResult = await this.transferTokenCommon(transferCommonParams);
    return transferResult;
  }

  /**
   * @summary Single Transfer ERC-20 Token
   * @param {TransferERC20Params} transferParams Transfer ERC-20 Tokens params (include information for Sender/Receiver)
   * @description After transfer was triggered, we can query the status of the transaction with the following functions:
   * + getTransactionDetails(transactionId: number) {return TransactionData} 
   * + getTransactionsByPartnerRefID(partnerRefID: string) {return []TransactionData} 
   * @returns {TransferResponse} Transactions data which indicate the status of batch, transaction results, transaction details information
   * (such as transactionID, transactionStatus...)
   * @throws {string} Exception: Sender vault is not found
   * @throws {string} Exception: Receiver vault is not found
   * @throws {string} Http Status 400 - Sender/Receiver vaults do not exist
   * @throws {string} Http Status 400 - Signature is invalid
   * @throws {string} Http Status 400 - Vault IDs does not have enough funds
   */
  public async transferETHToken(
    transferParams: TransferETHParams
  ): Promise<TransferResponse> {
    const requestSenderVaultERC20: VaultForETH = {
      starkKey: transferParams.senderPublicKey,
    };

    const requestReceiverVaultERC20: VaultForETH = {
      starkKey: transferParams.receiverPublicKey,
    };
    const senderVault = await this.commonAPI.retrieveETHVault(
      requestSenderVaultERC20
    );

    if (senderVault.status !== "success") {
      throw Error(
        '"Request sender vault failed => ' +
          JSON.stringify(requestSenderVaultERC20)
      );
    }

    const receiverVault = await this.commonAPI.retrieveETHVault(
      requestReceiverVaultERC20
    );
    if (receiverVault.status !== "success") {
      throw Error(
        '"Request receiver vault failed => ' + JSON.stringify(receiverVault)
      );
    }

    const transferCommonParams: TransferCommonParams = {
      senderVaultId: senderVault.data?.vaultId,
      senderPublicKey: transferParams.senderPublicKey,
      senderWalletAddress: transferParams.senderWalletAddress,
      receiverVaultId: receiverVault.data?.vaultId,
      receiverPublicKey: transferParams.receiverPublicKey,
      assetId: senderVault.data?.assetId,
      quantizedAmount: transferParams.quantizedAmount,
      tokenType: TokenType.ETH,
      groupRequestId: transferParams.groupRequestId,
      partnerRefId: transferParams.partnerRefId,
      myriaPrivateStarkKey: transferParams.myriaPrivateKey,
      path: '/v1/transactions/transfer'
    };

    const transferResult = await this.transferTokenCommon(transferCommonParams);
    return transferResult;
  }

  public async transferTokenCommon(
    transferParams: TransferCommonParams
  ): Promise<TransferResponse> {
    if (!transferParams.senderPublicKey) {
      throw new Error("Sender public key is required.");
    }

    if (!transferParams.senderVaultId) {
      throw new Error("Sender vault Id is required.");
    }

    if (!transferParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required!");
    }

    if (!transferParams.receiverPublicKey) {
      throw new Error("Receiver public key is required!");
    }

    if (!transferParams.receiverVaultId) {
      throw new Error("Receiver vault Id is required!");
    }

    if (!transferParams.assetId) {
      throw new Error("Asset id is required!");
    }

    if (!transferParams.quantizedAmount) {
      throw new Error("Amount is required!");
    }

    // if (!transferParams.myriaPrivateStarkKey) {
    //   throw new Error('Myria private stark key is required!');
    // }

    const currentTimestamp = new Date();
    // const nonce = transferParams.nonce || currentTimestamp.getDate() * Math.floor(Math.random() * 10);
    let nonce: number;

    try {
      const result = await this.commonAPI.getNonceByStarkKey(
        transferParams.senderPublicKey
      );
      if (result.status === "success") {
        nonce = result.data;
      } else {
        throw new Error("Getting nonce value failed!");
      }
    } catch (err) {
      throw new Error(`Getting nonce value failed: ${err}`);
    }

    const expirationTimestamp =
      transferParams.expirationTimestamp ||
      Math.floor(
        currentTimestamp.setFullYear(currentTimestamp.getFullYear() + 20) /
          3600 /
          1000
      );

    if (!transferParams.nonce) {
      transferParams = {
        ...transferParams,
        nonce,
      };
    }

    if (!transferParams.expirationTimestamp) {
      transferParams = {
        ...transferParams,
        expirationTimestamp,
      };
    }

    const starkSignature =
      await this.commonModule.generateStarkSignatureForTransfer(transferParams);

    if (!starkSignature) {
      throw new Error("Signing message failed!");
    }

    const currentTimestampSignatureHeader = Math.floor(Date.now() / 1000);

    let generateSignatureHeader = undefined;
    if (transferParams.myriaPrivateStarkKey) {
      generateSignatureHeader =
        await this.commonModule.generateHeaderSignatureWithSecretKey(
          transferParams.myriaPrivateStarkKey || "",
          transferParams.senderPublicKey,
          currentTimestampSignatureHeader,
          transferParams.path || ""
        );
    }

    const transferRequest: TransferAPIInput = {
      senderVaultId: transferParams.senderVaultId,
      senderPublicKey: transferParams.senderPublicKey,
      receiverVaultId: transferParams.receiverVaultId,
      receiverPublicKey: transferParams.receiverPublicKey,
      token: transferParams.assetId,
      quantizedAmount: transferParams.quantizedAmount,
      nonce: Number(transferParams.nonce),
      expirationTimestamp: Number(transferParams.expirationTimestamp),
      signature: starkSignature,
      signatureHeader: generateSignatureHeader,
    };

    let transferResult: TransferResponse;

    try {
      let transferResponse = undefined;
      if (
        transferParams.tokenType === TokenType.ERC20 ||
        transferParams.tokenType === TokenType.MINTABLE_ERC20 ||
        transferParams.tokenType === TokenType.ETH
      ) {
        transferResponse = await this.transactionAPI.transferToken(
          transferRequest
        );
      } else if (
        transferParams.tokenType === TokenType.ERC721 ||
        transferParams.tokenType === TokenType.MINTABLE_ERC721
      ) {
        transferResponse = await this.assetMarketplaceAPI.transferERC721Token(
          transferRequest
        );
      }

      if (!transferResponse) {
        throw Error(
          "Please check token type, the transfer response is undefined"
        );
      }

      if (transferResponse?.status === "success") {
        transferResult = transferResponse?.data;
      } else {
        const error: ICommonError = {
          message: "Transfer failed with server error",
          code: "",
          error: undefined
        }
        throw new Error(JSON.stringify(error));
      }
    } catch (err) {
      const error: ICommonError = {
        message: "Transfer failed with server error",
        code: "",
        error: err
      }
      throw new Error(JSON.stringify(error));
    }

    return transferResult;
  }

  public async updateTransactionComplete(
    payload: TransactionCompleteParams
  ): Promise<APIResponseType<TransactionCompleteResponse>> {
    if (!payload.starkKey) {
      throw new Error("Stark key is required");
    }
    if (!payload.transactionHash) {
      throw new Error("Transaction hash is required");
    }
    if (!payload.transactionId) {
      throw new Error("Transaction id is required");
    }

    let response: APIResponseType<TransactionCompleteResponse>;
    try {
      response = await this.transactionAPI.updateTransactionComplete(payload);
    } catch (err) {
      throw new Error(`Error api called ${err}`);
    }
    return response;
  }

  /**
   * @summary Asynchronous burn for NFT Tokens (such as: ERC-721 Tokens, Marketplace NFTs) 
   * - Function only supports MINTABLE_ERC721 and NFTs which are minted on Myria System
   * @param {BurnTokenParams} burnTokenParams Data regarding sender and receivers relevant for the burn transfer.
   * @description After burn transfer was triggered, we can query the status of the batch with the following functions:
   * + getTransactionsByGroupRequestIDAndPartnerRefID(groupRequestID: string, partnerRefID: string)
   * + getTransactionsByPartnerRefID(partnerRefID: string)
   * @returns {BurnTokenResponse} Transaction data list which have been captured and validated 
   * - Response structure consist of 3 group of transactions failed[] / success[] / validationFailed[]
   * - All transactions in failed[], relate to failures due to not enough funds or other internal server errors. These transactions cannot be processed.
   * - All transactions in validationFailed[], relate to failures due to validation such as L2's signature. These can be retried with amended data.
   * - All transactions in success[], indicate that they have been recorded and will be processed by the system.
   * @throws {string} Exception: Sender wallet address is required
   * @throws {string} Exception: Burn transfer should include at least one transfer
   * @throws {string} Exception: Only MINTABLE_ERC-721 tokens are valid for this type of bulk transfer
   * @throws {string} Exception: Token address is required
   * @throws {string} Error code 409 - Request-ID/Group-Request-ID is already exists
   * @throws {string} Http error code 400 - User wallet (sender or receiver) is not registered
   * @throws {string} Http error code 400 - Vault ID does not have enough funds
   * @throws {string} Http error code 400 - Signature is invalid
   * @example <caption>Sample code on Testnet (Staging) env</caption> 
   * 
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const YOUR_NFT_CONTRACT_ADDRESS = "0xA06116D9...."; 
    const SENDER_WALLET_ADDRESS = '0x724f337bF0F....'; // Must be the owner of tokens, sender wallet address

    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();

    const burnTransferredItems: ItemSignableBurnTransferParams[] = [
      {
        quantizedAmount: 1, // Should be 1 as always
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '1' // Your minted token ID,
          quantum: '1'
        },
      },
      {
        quantizedAmount: 1,
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '2' // Your minted token ID,
        },
      },
      {
        quantizedAmount: 1,
        tokenType: TokenType.MINTABLE_ERC721,
        tokenData: {
          tokenAddress: YOUR_NFT_CONTRACT_ADDRESS,
          tokenId: '3' // Your minted token ID
        },
      },
    ];

    const burnTransferTokenParams: BurnTokenParams = {
      senderWalletAddress: SENDER_WALLET_ADDRESS,
      groupRequestId: '7257d29c-c96a-4302-8eaf-368a0d62b977', // Can use random UUID to generate groupRequestID
      requestId: '7257d29c-c96a-4302-8eaf-368a0d62b977',  // Can use random UUID to generate requestID
      partnerRefId: 'Project-ID', // Project-ID on Myria System
      description: 'Test-Test Burn Transfer',
      items: burnTransferredItems,
    };

    const burnTransferResult = await transactionManager.burnNfts(
      burnTransferTokenParams,
    );
   * 
   */
  public async burnNfts(
    burnTokenParams: BurnTokenParams
  ): Promise<APIResponseType<BurnTokenResponse>> {
    if (!burnTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (!burnTokenParams.items || burnTokenParams.items.length === 0) {
      throw new Error("Burn transfer should include at least one transfer.");
    }

    const transferredItems: ItemSignableBurnParams[] = [];

    burnTokenParams.items.forEach((item) => {
      if (item.tokenType !== (TokenType.ERC721 && TokenType.MINTABLE_ERC721)) {
        throw new Error(
          "Only MINTABLE_ERC-721 tokens are valid for this type of burn transfer"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }
      if (!item.tokenData?.tokenId) {
        throw new Error("Token ID is required for transfer ERC-721 tokens ");
      }
      const transferredItem: ItemSignableBurnParams = {
        quantizedAmount: item.quantizedAmount,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      transferredItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBurnTransferredParams: SignableBurnTokensParams = {
      senderWalletAddress: burnTokenParams.senderWalletAddress,
      items: transferredItems,
    };

    const fullPayloadBurnTransferData =
      await this.transactionAPI.signableBurnTokens(
        signableBurnTransferredParams
      );

    if (!fullPayloadBurnTransferData) {
      throw new Error("Burn Transfer payload data is required");
    }
    // Create generateFullPayloadForBurnTransfer list of signature
    const fullPayloadBurnTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        burnTokenParams.senderWalletAddress,
        TokenType.MINTABLE_ERC721,
        fullPayloadBurnTransferData,
        burnTokenParams.myriaPrivateKey
      );
    console.log("Full transfer payload -> ", fullPayloadBurnTransferred);

    // Trigger transfer
    const burnRequestApi: BurnTokensRequestAPIParams = {
      requestId: burnTokenParams.requestId,
      groupRequestId: burnTokenParams.groupRequestId,
      partnerRefId: burnTokenParams.partnerRefId,
      description: burnTokenParams.description,
      isWaitingForValidation: burnTokenParams.isWaitingForValidation,
      items: fullPayloadBurnTransferred,
    };

    const burnResult = await this.assetMarketplaceAPI.burnNfts(burnRequestApi);
    console.log(
      "Burn Transfer Response Nft API -> ",
      JSON.stringify(burnResult)
    );
    return burnResult;
  }

  /**
   * @summary Asynchronous burn for ERC-20 Tokens or Ethers Tokens (such as: ERC-20, Myria Tokens, Eth) 
   * - Function only supports ERC-20 standard tokens and Eth on Myria System
   * @param {BurnTokenParams} burnTransferTokenParams Data regarding sender and receivers relevant for the burn.
   * @description After burn transfer was triggered, we can query the status of the batch with the following functions:
   * + getTransactionsByGroupRequestIDAndPartnerRefID(groupRequestID: string, partnerRefID: string)
   * + getTransactionsByPartnerRefID(partnerRefID: string)
   * @returns {BurnTokenResponse} Transaction data list which have been captured and validated 
   * - Response structure consist of 3 group of transactions failed[] / success[] / validationFailed[]
   * - All transactions in failed[], relate to failures due to not enough funds or other internal server errors. These transactions cannot be processed.
   * - All transactions in validationFailed[], relate to failures due to validation such as L2's signature. These can be retried with amended data.
   * - All transactions in success[], indicate that they have been recorded and will be processed by the system.
   * @throws {string} Exception: Sender wallet address is required
   * @throws {string} Exception: Burn transfer should include at least one transfer
   * @throws {string} Exception: Only ERC-20 or Eth tokens are valided for this burn action
   * @throws {string} Exception: Token address is required
   * @throws {string} Error code 409 - Request-ID/Group-Request-ID is already exists
   * @throws {string} Http error code 400 - User wallet (sender or receiver) is not registered
   * @throws {string} Http error code 400 - Vault ID does not have enough funds
   * @throws {string} Http error code 400 - Signature is invalid
   * @example <caption>Sample code on Testnet (Staging) env</caption> 
   * 
    const mClient: IMyriaClient = {
      networkId: Network.SEPOLIA,
      provider: web3Instance.currentProvider,
      web3: web3Instance,
      env: EnvTypes.STAGING,
    };

    const YOUR_TOKEN_CONTRACT_ADDRESS = "0xA06116D9...."; 
    const SENDER_WALLET_ADDRESS = '0x724f337bF0F....'; // Must be the owner of tokens, sender wallet address

    const moduleFactory = ModuleFactory.getInstance(mClient);
    const transactionManager = moduleFactory.getTransactionManager();

    const QUANTUM = 10000000000;
    const burnTransferredItems: ItemSignableBurnParams[] = [
      {
        quantizedAmount: 1, // Should be 1 as always
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: YOUR_TOKEN_CONTRACT_ADDRESS,
          quantum: QUANTUM
        },
      },
      {
        quantizedAmount: 1,
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: YOUR_TOKEN_CONTRACT_ADDRESS,
          quantum: QUANTUM
        },
      },
      {
        quantizedAmount: 1,
        tokenType: TokenType.ERC20,
        tokenData: {
          tokenAddress: YOUR_TOKEN_CONTRACT_ADDRESS,
          quantum: QUANTUM
        },
      },
    ];

    const burnTokensParams: BurnTokenParams = {
      senderWalletAddress: SENDER_WALLET_ADDRESS,
      groupRequestId: '7257d29c-c96a-4302-8eaf-368a0d62b977', // Can use random UUID to generate groupRequestID
      requestId: '7257d29c-c96a-4302-8eaf-368a0d62b977',  // Can use random UUID to generate requestID
      partnerRefId: 'Project-ID', // Project-ID on Myria System
      description: 'Test-Test Burn Transfer',
      items: burnTransferredItems,
    };

    const burnTransferResult = await transactionManager.burnERC20Tokens(
      burnTransferTokenParams,
    );
   * 
   */
  public async burnERC20Tokens(
    burnTransferTokenParams: BurnTokenParams
  ): Promise<APIResponseType<BurnTokenResponse>> {
    if (!burnTransferTokenParams.senderWalletAddress) {
      throw new Error("Sender wallet address is required");
    }

    if (
      !burnTransferTokenParams.items ||
      burnTransferTokenParams.items.length === 0
    ) {
      throw new Error("Burn transfer should include at least one transfer.");
    }

    const burnedItems: ItemSignableBurnParams[] = [];

    burnTransferTokenParams.items.forEach((item) => {
      if (
        item.tokenType !== TokenType.ERC20 &&
        item.tokenType !== TokenType.MINTABLE_ERC20
      ) {
        throw new Error(
          "Only ERC-20 or MINTABLE_ERC20 tokens are valided for this burn action"
        );
      }
      if (!item.tokenData?.tokenAddress) {
        throw new Error("Token address is required");
      }

      const transferredItem: ItemSignableBurnParams = {
        quantizedAmount: item?.quantizedAmount,
        tokenType: item.tokenType,
        tokenData: item.tokenData,
      };

      burnedItems.push(transferredItem);
    });

    // Call signable API endpoint
    const signableBurnTransferredParams: SignableBurnTokensParams = {
      senderWalletAddress: burnTransferTokenParams.senderWalletAddress,
      items: burnedItems,
    };

    const fullPayloadBurnTransferData =
      await this.transactionAPI.signableBurnTokens(
        signableBurnTransferredParams
      );

    if (!fullPayloadBurnTransferData) {
      throw new Error("Burn Transfer payload data is required");
    }
    // Create generateFullPayloadForBurnTransfer list of signature
    const fullPayloadBurnTransferred =
      await this.commonModule.generateFullPayloadForBulkTransfer(
        burnTransferTokenParams.senderWalletAddress,
        TokenType.ERC20,
        fullPayloadBurnTransferData,
        burnTransferTokenParams.myriaPrivateKey
      );
    console.log("Full transfer payload -> ", fullPayloadBurnTransferred);

    // Trigger transfer
    const burnRequestApi: BurnTokensRequestAPIParams = {
      requestId: burnTransferTokenParams.requestId,
      groupRequestId: burnTransferTokenParams.groupRequestId,
      partnerRefId: burnTransferTokenParams.partnerRefId,
      description: burnTransferTokenParams.description,
      isWaitingForValidation: burnTransferTokenParams.isWaitingForValidation,
      items: fullPayloadBurnTransferred,
    };

    const burnedResult = await this.transactionAPI.burnTokens(burnRequestApi);
    console.log(
      "Burn Transfer Response Nft API -> ",
      JSON.stringify(burnedResult)
    );
    return burnedResult;
  }

  /**
   * @summary Get list of tokens supported in Myria
   * @description Get details tokens list data which is supported in Myria including TokenHex, TokenQuantum...
   * @returns {WhitelistTokensResponse} Whitelist tokens data
   **/
  public async getSupportedTokens(): Promise<WhitelistTokensResponse> {
    const whitelistTokens = await this.transactionAPI.getWhitelistTokens();
    return whitelistTokens;
  }
}