oceanprotocol/ocean.js

View on GitHub
src/services/Provider.ts

Summary

Maintainability
F
1 wk
Test Coverage
F
59%
import fetch from 'cross-fetch'
import { ethers, Signer, providers } from 'ethers'
import { LoggerInstance } from '../utils'
import {
  Arweave,
  FileInfo,
  ComputeJob,
  ComputeOutput,
  ComputeAlgorithm,
  ComputeAsset,
  ComputeEnvironment,
  ProviderInitialize,
  ProviderComputeInitializeResults,
  ServiceEndpoint,
  UrlFile,
  UserCustomParameters,
  Ipfs,
  Smartcontract,
  GraphqlQuery
} from '../@types'

export class Provider {
  /**
   * Returns the provider endpoints
   * @param {string} providerUri - the provider url
   * @return {Promise<any>}
   */
  async getEndpoints(providerUri: string): Promise<any> {
    try {
      const endpoints = await this.getData(providerUri)
      return await endpoints.json()
    } catch (e) {
      LoggerInstance.error('Finding the service endpoints failed:', e)
      throw new Error('HTTP request failed calling Provider')
    }
  }

  /**
   * This function returns the endpoint URL for a given service name.
   * @param {ServiceEndpoint[]} servicesEndpoints - The array of service endpoints
   * @param {string} serviceName - The name of the service
   * @returns {ServiceEndpoint} The endpoint URL for the given service name
   */
  getEndpointURL(
    servicesEndpoints: ServiceEndpoint[],
    serviceName: string
  ): ServiceEndpoint {
    if (!servicesEndpoints) return null
    return servicesEndpoints.find(
      (s) => s.serviceName.toLowerCase() === serviceName.toLowerCase()
    ) as ServiceEndpoint
  }

  /**
   * This function returns an array of service endpoints for a given provider endpoint.
   * @param {string} providerEndpoint - The provider endpoint
   * @param {any} endpoints - The endpoints object
   * @returns {ServiceEndpoint[]} An array of service endpoints
   */
  public async getServiceEndpoints(providerEndpoint: string, endpoints: any) {
    const serviceEndpoints: ServiceEndpoint[] = []
    for (const i in endpoints.serviceEndpoints) {
      const endpoint: ServiceEndpoint = {
        serviceName: i,
        method: endpoints.serviceEndpoints[i][0],
        urlPath:
          providerEndpoint.replace(/\/+$/, '') +
          '/' +
          endpoints.serviceEndpoints[i][1].replace(/^\/+/, '')
      }
      serviceEndpoints.push(endpoint)
    }
    return serviceEndpoints
  }

  /**
   * Get current nonce from the provider.
   * @param {string} providerUri provider uri address
   * @param {string} consumerAddress Publisher address
   * @param {AbortSignal} signal abort signal
   * @param {string} providerEndpoints Identifier of the asset to be registered in ocean
   * @param {string} serviceEndpoints document description object (DDO)=
   * @return {Promise<string>} urlDetails
   */
  public async getNonce(
    providerUri: string,
    consumerAddress: string,
    signal?: AbortSignal,
    providerEndpoints?: any,
    serviceEndpoints?: ServiceEndpoint[]
  ): Promise<number> {
    if (!providerEndpoints) {
      providerEndpoints = await this.getEndpoints(providerUri)
    }
    if (!serviceEndpoints) {
      serviceEndpoints = await this.getServiceEndpoints(providerUri, providerEndpoints)
    }
    const path = this.getEndpointURL(serviceEndpoints, 'nonce')
      ? this.getEndpointURL(serviceEndpoints, 'nonce').urlPath
      : null
    if (!path) return null
    try {
      const response = await fetch(path + `?userAddress=${consumerAddress}`, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
        signal
      })
      const { nonce } = await response.json()
      console.log(`[getNonce] Consumer: ${consumerAddress} nonce: ${nonce}`)
      const sanitizedNonce = !nonce || nonce === null ? 0 : Number(nonce)
      return sanitizedNonce
    } catch (e) {
      LoggerInstance.error(e)
      throw new Error(e.message)
    }
  }

  /**
   * Sign a provider request with a signer.
   * @param {Signer} signer - The signer to use.
   * @param {string} message - The message to sign.
   * @returns {Promise<string>} A promise that resolves with the signature.
   */
  public async signProviderRequest(signer: Signer, message: string): Promise<string> {
    //  const isMetaMask = web3 && web3.currentProvider && (web3.currentProvider as any).isMetaMask
    //  if (isMetaMask) return await web3.eth.personal.sign(consumerMessage, accountId, password)
    //  await web3.eth.sign(consumerMessage, await signer.getAddress())
    const consumerMessage = ethers.utils.solidityKeccak256(
      ['bytes'],
      [ethers.utils.hexlify(ethers.utils.toUtf8Bytes(message))]
    )
    const messageHashBytes = ethers.utils.arrayify(consumerMessage)
    const chainId = await signer.getChainId()
    try {
      return await signer.signMessage(messageHashBytes)
    } catch (error) {
      LoggerInstance.error('Sign provider message error: ', error)
      if (chainId === 8996) {
        return await (signer as providers.JsonRpcSigner)._legacySignMessage(
          messageHashBytes
        )
      }
    }
  }

  /**
   * Encrypt data using the Provider's own symmetric key
   * @param {string} data data in json format that needs to be sent , it can either be a DDO or a File array
   * @param {number} chainId network's id so provider can choose the corresponding Signer object
   * @param {string} providerUri provider uri address
   * @param {AbortSignal} signal abort signal
   * @return {Promise<string>} urlDetails
   */
  public async encrypt(
    data: any,
    chainId: number,
    providerUri: string,
    signal?: AbortSignal
  ): Promise<string> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const path =
      (this.getEndpointURL(serviceEndpoints, 'encrypt')
        ? this.getEndpointURL(serviceEndpoints, 'encrypt').urlPath
        : null) + `?chainId=${chainId}`
    if (!path) return null
    try {
      const response = await fetch(path, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/octet-stream' },
        signal
      })
      return await response.text()
    } catch (e) {
      LoggerInstance.error(e)
      throw new Error('HTTP request failed calling Provider')
    }
  }

  /**
   * Get file details for a given DID and service ID.
   * @param {string} did - The DID to check.
   * @param {string} serviceId - The service ID to check.
   * @param {string} providerUri - The URI of the provider.
   * @param {boolean} [withChecksum=false] - Whether or not to include a checksum.
   * @param {AbortSignal} [signal] - An optional abort signal.
   * @returns {Promise<FileInfo[]>} A promise that resolves with an array of file info objects.
   */
  public async checkDidFiles(
    did: string,
    serviceId: string,
    providerUri: string,
    withChecksum: boolean = false,
    signal?: AbortSignal
  ): Promise<FileInfo[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const args = { did, serviceId, checksum: withChecksum }
    const files: FileInfo[] = []
    const path = this.getEndpointURL(serviceEndpoints, 'fileinfo')
      ? this.getEndpointURL(serviceEndpoints, 'fileinfo').urlPath
      : null
    if (!path) return null
    let response
    try {
      response = await fetch(path, {
        method: 'POST',
        body: JSON.stringify(args),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('File info call failed: ')
      LoggerInstance.error(e)
      throw new Error(e)
    }
    if (response?.ok) {
      const results: FileInfo[] = await response.json()
      for (const result of results) {
        files.push(result)
      }
      return files
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'File info call failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /**
   * Get File details (if possible)
   * @param {UrlFile | Arweave | Ipfs | GraphqlQuery | Smartcontract} file one of the supported file structures
   * @param {string} providerUri uri of the provider that will be used to check the file
   * @param {boolean} [withChecksum=false] - Whether or not to include a checksum.
   * @param {AbortSignal} [signal] - An optional abort signal.
   * @returns {Promise<FileInfo[]>} A promise that resolves with an array of file info objects.
   */
  public async getFileInfo(
    file: UrlFile | Arweave | Ipfs | GraphqlQuery | Smartcontract,
    providerUri: string,
    withChecksum: boolean = false,
    signal?: AbortSignal
  ): Promise<FileInfo[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const args = { ...file, checksum: withChecksum }
    const files: FileInfo[] = []
    const path = this.getEndpointURL(serviceEndpoints, 'fileinfo')
      ? this.getEndpointURL(serviceEndpoints, 'fileinfo').urlPath
      : null
    if (!path) return null
    let response
    try {
      response = await fetch(path, {
        method: 'POST',
        body: JSON.stringify(args),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('File info call failed: ')
      LoggerInstance.error(e)
      throw new Error(e)
    }
    if (response?.ok) {
      const results: FileInfo[] = await response.json()
      for (const result of results) {
        files.push(result)
      }
      return files
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'File info call failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /**
   * Returns compute environments from a provider.
   * @param {string} providerUri - The URI of the provider.
   * @param {AbortSignal} [signal] - An optional abort signal.
   * @returns {Promise<{[chainId: number]: ComputeEnvironment[]}>} A promise that resolves with an object containing compute environments for each chain ID.
   */
  public async getComputeEnvironments(
    providerUri: string,
    signal?: AbortSignal
  ): Promise<{ [chainId: number]: ComputeEnvironment[] }> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const path = this.getEndpointURL(serviceEndpoints, 'computeEnvironments')?.urlPath
    if (!path) return null
    let response
    try {
      response = await fetch(path, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Fetch compute env failed: ')
      LoggerInstance.error(e)
      throw new Error(e)
    }
    if (response?.ok) {
      const result = response.json()
      if (Array.isArray(result)) {
        const providerChain: number = providerEndpoints.chainId
        return { [providerChain]: result }
      }
      return result
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Fetch compute env failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /**
   * Initializes the provider for a service request.
   * @param {string} did - The asset DID .
   * @param {string} serviceId - The asset service ID.
   * @param {number} fileIndex - The file index.
   * @param {string} consumerAddress - The consumer address.
   * @param {string} providerUri - The URI of the provider.
   * @param {AbortSignal} [signal] - The abort signal if any.
   * @param {UserCustomParameters} [userCustomParameters] - The custom parameters if any.
   * @param {string} [computeEnv] - The compute environment if any.
   * @param {number} [validUntil] - The validity time if any.
   * @returns {Promise<ProviderInitialize>} A promise that resolves with ProviderInitialize response.
   */
  public async initialize(
    did: string,
    serviceId: string,
    fileIndex: number,
    consumerAddress: string,
    providerUri: string,
    signal?: AbortSignal,
    userCustomParameters?: UserCustomParameters,
    computeEnv?: string,
    validUntil?: number
  ): Promise<ProviderInitialize> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    let initializeUrl = this.getEndpointURL(serviceEndpoints, 'initialize')
      ? this.getEndpointURL(serviceEndpoints, 'initialize').urlPath
      : null

    if (!initializeUrl) return null
    initializeUrl += `?documentId=${did}`
    initializeUrl += `&serviceId=${serviceId}`
    initializeUrl += `&fileIndex=${fileIndex}`
    initializeUrl += `&consumerAddress=${consumerAddress}`
    if (userCustomParameters)
      initializeUrl += '&userdata=' + encodeURI(JSON.stringify(userCustomParameters))
    if (computeEnv) initializeUrl += '&environment=' + encodeURI(computeEnv)
    if (validUntil) initializeUrl += '&validUntil=' + validUntil
    let response
    try {
      response = await fetch(initializeUrl, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Provider initialized failed: ')
      LoggerInstance.error(e)
      throw new Error(`Provider initialize failed url: ${initializeUrl} `)
    }
    if (response?.status === 200) {
      const results: ProviderInitialize = await response.json()
      return results
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Provider initialized failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /** Initializes the provider for a compute request.
   * @param {ComputeAsset[]} assets The datasets array to initialize compute request.
   * @param {ComputeAlgorithmber} algorithm The algorithm to use.
   * @param {string} computeEnv The compute environment.
   * @param {number} validUntil  The job expiration date.
   * @param {string} providerUri The provider URI.
   * @param {string} accountId caller address
   * @param {AbortSignal} signal abort signal
   * @return {Promise<ProviderComputeInitialize>} ProviderComputeInitialize data
   */
  public async initializeCompute(
    assets: ComputeAsset[],
    algorithm: ComputeAlgorithm,
    computeEnv: string,
    validUntil: number,
    providerUri: string,
    accountId: string,
    signal?: AbortSignal
  ): Promise<ProviderComputeInitializeResults> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const providerData = {
      datasets: assets,
      algorithm,
      compute: { env: computeEnv, validUntil },
      consumerAddress: accountId
    }
    const initializeUrl = this.getEndpointURL(serviceEndpoints, 'initializeCompute')
      ? this.getEndpointURL(serviceEndpoints, 'initializeCompute').urlPath
      : null
    if (!initializeUrl) return null
    let response
    try {
      response = await fetch(initializeUrl, {
        method: 'POST',
        body: JSON.stringify(providerData),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Initialize compute failed: ')
      LoggerInstance.error(e)
      throw new Error('ComputeJob cannot be initialized')
    }
    if (response?.ok) {
      const params = await response.json()
      return params
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Initialize compute failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    LoggerInstance.error('Payload was:', providerData)
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /**
   * Gets the download URL.
   * @param {string} did - The DID.
   * @param {string} serviceId - The service ID.
   * @param {number} fileIndex - The file index.
   * @param {string} transferTxId - The transfer transaction ID.
   * @param {string} providerUri - The provider URI.
   * @param {Signer} signer - The signer.
   * @param {UserCustomParameters} userCustomParameters - The user custom parameters.
   * @returns {Promise<any>} The download URL.
   */
  public async getDownloadUrl(
    did: string,
    serviceId: string,
    fileIndex: number,
    transferTxId: string,
    providerUri: string,
    signer: Signer,
    userCustomParameters?: UserCustomParameters
  ): Promise<any> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const downloadUrl = this.getEndpointURL(serviceEndpoints, 'download')
      ? this.getEndpointURL(serviceEndpoints, 'download').urlPath
      : null
    if (!downloadUrl) return null
    const consumerAddress = await signer.getAddress()
    const nonce = (
      (await this.getNonce(
        providerUri,
        consumerAddress,
        null,
        providerEndpoints,
        serviceEndpoints
      )) + 1
    ).toString()

    const signature = await this.signProviderRequest(signer, did + nonce) // did + nonce
    let consumeUrl = downloadUrl
    consumeUrl += `?fileIndex=${fileIndex}`
    consumeUrl += `&documentId=${did}`
    consumeUrl += `&transferTxId=${transferTxId}`
    consumeUrl += `&serviceId=${serviceId}`
    consumeUrl += `&consumerAddress=${consumerAddress}`
    consumeUrl += `&nonce=${nonce}`
    consumeUrl += `&signature=${signature}`
    if (userCustomParameters)
      consumeUrl += '&userdata=' + encodeURI(JSON.stringify(userCustomParameters))
    return consumeUrl
  }

  /** Instruct the provider to start a compute job
   * @param {string} providerUri The provider URI.
   * @param {Signer} signer The consumer signer object.
   * @param {string} computeEnv The compute environment.
   * @param {ComputeAsset} dataset The dataset to start compute on
   * @param {ComputeAlgorithm} algorithm The algorithm to start compute with.
   * @param {AbortSignal} signal abort signal
   * @param {ComputeAsset[]} additionalDatasets The additional datasets if that is the case.
   * @param {ComputeOutput} output The compute job output settings.
   * @return {Promise<ComputeJob | ComputeJob[]>} The compute job or jobs.
   */
  public async computeStart(
    providerUri: string,
    consumer: Signer,
    computeEnv: string,
    dataset: ComputeAsset,
    algorithm: ComputeAlgorithm,
    signal?: AbortSignal,
    additionalDatasets?: ComputeAsset[],
    output?: ComputeOutput
  ): Promise<ComputeJob | ComputeJob[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const computeStartUrl = this.getEndpointURL(serviceEndpoints, 'computeStart')
      ? this.getEndpointURL(serviceEndpoints, 'computeStart').urlPath
      : null

    const consumerAddress = await consumer.getAddress()
    const nonce = (
      (await this.getNonce(
        providerUri,
        consumerAddress,
        signal,
        providerEndpoints,
        serviceEndpoints
      )) + 1
    ).toString()

    let signatureMessage = consumerAddress
    signatureMessage += dataset.documentId
    signatureMessage += nonce
    const signature = await this.signProviderRequest(consumer, signatureMessage)
    const payload = Object()
    payload.consumerAddress = consumerAddress
    payload.signature = signature
    payload.nonce = nonce
    payload.environment = computeEnv
    payload.dataset = dataset
    payload.algorithm = algorithm
    if (additionalDatasets) payload.additionalDatasets = additionalDatasets
    if (output) payload.output = output
    if (!computeStartUrl) return null
    let response
    try {
      response = await fetch(computeStartUrl, {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Compute start failed:')
      LoggerInstance.error(e)
      LoggerInstance.error('Payload was:', payload)
      throw new Error('HTTP request failed calling Provider')
    }
    if (response?.ok) {
      const params = await response.json()
      return params
    }
    LoggerInstance.error(
      'Compute start failed: ',
      response.status,
      response.statusText,
      await response.json()
    )
    LoggerInstance.error('Payload was:', payload)
    return null
  }

  /** Instruct the provider to Stop the execution of a to stop a compute job.
   * @param {string} did the asset did
   * @param {string} consumerAddress The consumer address.
   * @param {string} jobId the compute job id
   * @param {string} providerUri The provider URI.
   * @param {Signer} signer The consumer signer object.
   * @param {AbortSignal} signal abort signal
   * @return {Promise<ComputeJob | ComputeJob[]>}
   */
  public async computeStop(
    did: string,
    consumerAddress: string,
    jobId: string,
    providerUri: string,
    signer: Signer,
    agreementId?: string,
    signal?: AbortSignal
  ): Promise<ComputeJob | ComputeJob[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const computeStopUrl = this.getEndpointURL(serviceEndpoints, 'computeStop')
      ? this.getEndpointURL(serviceEndpoints, 'computeStop').urlPath
      : null

    const nonce = (
      (await this.getNonce(
        providerUri,
        consumerAddress,
        signal,
        providerEndpoints,
        serviceEndpoints
      )) + 1
    ).toString()

    let signatureMessage = consumerAddress
    signatureMessage += jobId || ''
    // On current provider impl (and nodes) we DO NOT check this signature
    // On nodes we are signing again just the Nonce to send the request to Operator Service
    // on current provider we sign: {owner}{job_id}{nonce}" OR {owner}{nonce} if no jobId
    // On provider service STOP route, we just check signature owner + jobId OR just owner if no jobId
    // signatureMessage += (agreementId && `${this.noZeroX(agreementId)}`) || ''
    // signatureMessage += nonce
    const signature = await this.signProviderRequest(signer, signatureMessage)
    const payload = Object()
    payload.signature = signature
    payload.agreementId = this.noZeroX(agreementId)
    payload.consumerAddress = consumerAddress
    payload.nonce = nonce
    if (jobId) payload.jobId = jobId

    if (!computeStopUrl) return null
    let response
    try {
      response = await fetch(computeStopUrl, {
        method: 'PUT',
        body: JSON.stringify(payload),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Compute stop failed:')
      LoggerInstance.error(e)
      LoggerInstance.error('Payload was:', payload)
      throw new Error('HTTP request failed calling Provider')
    }

    if (response?.ok) {
      const params = await response.json()
      return params
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Compute stop failed: ',
      response.status,
      response.statusText,
      resolvedResponse
    )
    LoggerInstance.error('Payload was:', payload)
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /** Get compute status for a specific jobId/documentId/owner.
   * @param {string} providerUri The URI of the provider we want to query
   * @param {string} consumerAddress The consumer ethereum address
   * @param {string} jobId The ID of a compute job.
   * @param {string} agreementId The ID of the service agreement (tx id)
   * @param {AbortSignal} signal abort signal
   * @return {Promise<ComputeJob | ComputeJob[]>}
   */
  public async computeStatus(
    providerUri: string,
    consumerAddress: string,
    jobId?: string,
    agreementId?: string,
    signal?: AbortSignal
  ): Promise<ComputeJob | ComputeJob[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const computeStatusUrl = this.getEndpointURL(serviceEndpoints, 'computeStatus')
      ? this.getEndpointURL(serviceEndpoints, 'computeStatus').urlPath
      : null

    let url = `?consumerAddress=${consumerAddress}`
    url += (agreementId && `&agreementId=${this.noZeroX(agreementId)}`) || ''
    url += (jobId && `&jobId=${jobId}`) || ''

    if (!computeStatusUrl) return null
    let response
    try {
      response = await fetch(computeStatusUrl + url, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Get compute status failed')
      LoggerInstance.error(e)
      throw new Error(e)
    }
    if (response?.ok) {
      const params = await response.json()
      return params
    }
    LoggerInstance.error(
      'Get compute status failed:',
      response.status,
      response.statusText
    )
    if (response?.ok) {
      const params = await response.json()
      return params
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Get compute status failed:',
      response.status,
      response.statusText,
      resolvedResponse
    )
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /** Get compute result url
   * @param {string} providerUri The URI of the provider we want to query
   * @param {Signer} consumer consumer Signer wallet object
   * @param {string} jobId The ID of a compute job.
   * @param {number} index Result index
   * @return {Promise<string>}
   */
  public async getComputeResultUrl(
    providerUri: string,
    consumer: Signer,
    jobId: string,
    index: number
  ): Promise<string> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const computeResultUrl = this.getEndpointURL(serviceEndpoints, 'computeResult')
      ? this.getEndpointURL(serviceEndpoints, 'computeResult').urlPath
      : null

    const nonce = (
      (await this.getNonce(
        providerUri,
        await consumer.getAddress(),
        null,
        providerEndpoints,
        serviceEndpoints
      )) + 1
    ).toString()
    let signatureMessage = await consumer.getAddress()
    signatureMessage += jobId
    signatureMessage += index.toString()
    signatureMessage += nonce
    const signature = await this.signProviderRequest(consumer, signatureMessage)
    if (!computeResultUrl) return null
    let resultUrl = computeResultUrl
    resultUrl += `?consumerAddress=${await consumer.getAddress()}`
    resultUrl += `&jobId=${jobId}`
    resultUrl += `&index=${index.toString()}`
    resultUrl += `&nonce=${nonce}`
    resultUrl += (signature && `&signature=${signature}`) || ''
    return resultUrl
  }

  /** Deletes a compute job.
   * @param {string} did asset did
   * @param {Signer} consumer consumer Signer wallet object
   * @param {string} jobId the compute job ID
   * @param {string} providerUri The URI of the provider we want to query
   * @param {AbortSignal} signal abort signal
   * @return {Promise<ComputeJob | ComputeJob[]>}
   */
  public async computeDelete(
    did: string,
    consumer: Signer,
    jobId: string,
    providerUri: string,
    signal?: AbortSignal
  ): Promise<ComputeJob | ComputeJob[]> {
    const providerEndpoints = await this.getEndpoints(providerUri)
    const serviceEndpoints = await this.getServiceEndpoints(
      providerUri,
      providerEndpoints
    )
    const computeDeleteUrl = this.getEndpointURL(serviceEndpoints, 'computeDelete')
      ? this.getEndpointURL(serviceEndpoints, 'computeDelete').urlPath
      : null

    const nonce = (
      (await this.getNonce(
        providerUri,
        await consumer.getAddress(),
        signal,
        providerEndpoints,
        serviceEndpoints
      )) + 1
    ).toString()

    let signatureMessage = await consumer.getAddress()
    signatureMessage += jobId || ''
    signatureMessage += (did && `${this.noZeroX(did)}`) || ''
    signatureMessage += nonce
    const signature = await this.signProviderRequest(consumer, signatureMessage)
    const payload = Object()
    payload.documentId = this.noZeroX(did)
    payload.consumerAddress = await consumer.getAddress()
    payload.jobId = jobId
    if (signature) payload.signature = signature

    if (!computeDeleteUrl) return null
    let response
    try {
      response = await fetch(computeDeleteUrl, {
        method: 'DELETE',
        body: JSON.stringify(payload),
        headers: { 'Content-Type': 'application/json' },
        signal
      })
    } catch (e) {
      LoggerInstance.error('Delete compute job failed:')
      LoggerInstance.error(e)
      LoggerInstance.error('Payload was:', payload)
      throw new Error('HTTP request failed calling Provider')
    }
    if (response?.ok) {
      const params = await response.json()
      return params
    }
    const resolvedResponse = await response.json()
    LoggerInstance.error(
      'Delete compute job failed:',
      response.status,
      response.statusText,
      resolvedResponse
    )
    LoggerInstance.error('Payload was:', payload)
    throw new Error(JSON.stringify(resolvedResponse))
  }

  /** Check for a valid provider at URL
   * @param {String} url provider uri address
   * @param {AbortSignal} signal abort signal
   * @return {Promise<boolean>} valid or not
   */
  public async isValidProvider(url: string, signal?: AbortSignal): Promise<boolean> {
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
        signal
      })
      if (response?.ok) {
        const params = await response.json()
        if (params && (params.providerAddress || params.providerAddresses)) return true
      }
      return false
    } catch (error) {
      LoggerInstance.error(`Error validating provider: ${error.message}`)
      return false
    }
  }

  /**
   * Private method that removes the leading 0x from a string.
   * @param {string} input - The input string.
   * @returns The transformed string.
   */
  private noZeroX(input: string): string {
    return this.zeroXTransformer(input, false)
  }

  /**
   * Private method that removes the leading 0x from a string.
   * @param {string} input - The input string.
   * @param {boolean} zeroOutput - Whether to include 0x in the output if the input is valid and zeroOutput is true.
   * @returns The transformed string.
   */
  private zeroXTransformer(input = '', zeroOutput: boolean): string {
    const { valid, output } = this.inputMatch(
      input,
      /^(?:0x)*([a-f0-9]+)$/i,
      'zeroXTransformer'
    )
    return (zeroOutput && valid ? '0x' : '') + output
  }

  /**
   * Private method that matches an input string against a regular expression and returns the first capture group.
   * @param {string} input - The input string to match.
   * @param {RegExp} regexp - The regular expression to match against.
   * @param {string} conversorName - The name of the method calling this function.
   * @returns An object with two properties: `valid` (a boolean indicating whether the input matched the regular expression) and `output` (the first capture group of the match, or the original input if there was no match).
   */
  private inputMatch(
    input: string,
    regexp: RegExp,
    conversorName: string
  ): { valid: boolean; output: string } {
    if (typeof input !== 'string') {
      LoggerInstance.debug('Not input string:')
      LoggerInstance.debug(input)
      throw new Error(`[${conversorName}] Expected string, input type: ${typeof input}`)
    }
    const match = input.match(regexp)
    if (!match) {
      LoggerInstance.warn(`[${conversorName}] Input transformation failed.`)
      return { valid: false, output: input }
    }
    return { valid: true, output: match[1] }
  }

  /**
   * Private method that fetches data from a URL using the GET method.
   * @param {string} url - The URL to fetch data from.
   * @returns A Promise that resolves to a Response object.
   */
  private async getData(url: string): Promise<Response> {
    return fetch(url, {
      method: 'GET',
      headers: {
        'Content-type': 'application/json'
      }
    })
  }
}

export const ProviderInstance = new Provider()