def initialize(): """Initialize a service request. In order to consume a data service the user is required to send a number of data tokens to the provider as defined in the Asset's service description in the Asset's DDO document. The data tokens are transferred via the ethereum blockchain network by requesting the user to sign an ERC20 `approveAndLock` transaction where the approval is given to the provider's ethereum account for the number of tokens required by the service. :return: json object as follows: { "from": <consumer-address>, "to": <receiver-address>, "numTokens": <tokens-amount-in-base> "dataToken": <data-token-contract-address>, "nonce": <nonce-used-in-consumer-signature> } """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( # noqa data, 'initialize', require_signature=False) url = get_asset_url_at_index(0, asset, provider_wallet) download_url = get_download_url(url, app.config['CONFIG_FILE']) valid, details = check_url_details(download_url) if not valid: logger.error( f'Error: Asset URL not found or not available. \n' f'Payload was: {data}', exc_info=1) return jsonify(error="Asset URL not found or not available."), 400 minter = get_datatoken_minter(asset, token_address) # Prepare the `transfer` tokens transaction with the appropriate number # of tokens required for this service # The consumer must sign and execute this transaction in order to be # able to consume the service approve_params = { "from": consumer_address, "to": minter, "numTokens": float(service.get_cost()), "dataToken": token_address, "nonce": user_nonce.get_nonce(consumer_address) } return Response(json.dumps(approve_params), 200, headers={'content-type': 'application/json'}) except Exception as e: logger.error(f'Error: {e}. \n' f'Payload was: {data}', exc_info=1) return jsonify(error=str(e)), 500
def initialize(): """Initialize a service request. In order to consume a data service the user is required to send a number of data tokens to the provider as defined in the Asset's service description in the Asset's DDO document. The data tokens are transferred via the ethereum blockchain network by requesting the user to sign an ERC20 `approveAndLock` transaction where the approval is given to the provider's ethereum account for the number of tokens required by the service. :return: """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( data, 'initialize', require_signature=False) service_id = data.get('serviceId') service_type = data.get('serviceType') # Prepare the `transfer` tokens transaction with the appropriate number of # tokens required for this service # The consumer must sign and execute this transaction in order to be able to # consume the service approve_params = { "from": consumer_address, "to": provider_acc.address, # FIXME: Replace this constant price number "numTokens": 5, #service.get_price(), "dataTokenAddress": token_address, } return Response(json.dumps(approve_params), 200, headers={'content-type': 'application/json'}) except Exception as e: logger.error(f'Error: {e}. \n' f'Payload was: {data}', exc_info=1) return jsonify(error=e), 500
def computeStart(): """Call the execution of a workflow. --- tags: - services consumes: - application/json parameters: - name: signature in: query description: Signature of (consumerAddress+jobId+documentId) to verify the consumer of this asset/compute job. The signature uses ethereum based signing method (see https://github.com/ethereum/EIPs/pull/683) type: string - name: consumerAddress in: query description: The consumer ethereum address. required: true type: string - name: algorithmDid in: query description: The DID of the algorithm Asset to be executed required: false type: string - name: algorithmMeta in: query description: json object that define the algorithm attributes and url or raw code required: false type: json string - name: output in: query description: json object that define the output section required: true type: json string responses: 200: description: Call to the operator-service was successful. 400: description: One of the required attributes is missing. 401: description: Consumer signature is invalid or failed verification 500: description: General server error """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( # noqa data, 'compute_start_job', additional_params=["transferTxId", "output"], require_signature=False) service_id = data.get('serviceId') service_type = data.get('serviceType') signature = data.get('signature') tx_id = data.get("transferTxId") # Verify that the number of required tokens has been # transferred to the provider's wallet. _tx, _order_log, _transfer_log = validate_order( consumer_address, token_address, float(service.get_cost()), tx_id, add_0x_prefix(did_to_id(did)) if did.startswith('did:') else did, service_id) validate_transfer_not_used_for_other_service(did, service_id, tx_id, consumer_address, token_address) record_consume_request(did, service_id, tx_id, consumer_address, token_address, service.get_cost()) algorithm_did = data.get('algorithmDid') algorithm_token_address = data.get('algorithmDataToken') algorithm_meta = data.get('algorithmMeta') algorithm_tx_id = data.get('algorithmTransferTxId') output_def = data.get('output', dict()) assert service_type == ServiceTypes.CLOUD_COMPUTE # Validate algorithm choice if not (algorithm_meta or algorithm_did): msg = f'Need an `algorithmMeta` or `algorithmDid` to run, otherwise don\'t bother.' # noqa logger.error(msg, exc_info=1) return jsonify(error=msg), 400 # algorithmDid also requires algorithmDataToken # and algorithmTransferTxId if algorithm_did: if not (algorithm_token_address and algorithm_tx_id): msg = ( f'Using `algorithmDid` requires the `algorithmDataToken` and ' # noqa f'`algorithmTransferTxId` values in the request payload. ' f'algorithmDataToken is the DataToken address for the algorithm asset. ' # noqa f'algorithmTransferTxId is the transaction id (hash) of transferring ' # noqa f'data tokens from consumer wallet to this providers wallet.' ) logger.error(msg, exc_info=1) return jsonify(error=msg), 400 # Consumer signature original_msg = f'{consumer_address}{did}' verify_signature(consumer_address, signature, original_msg, user_nonce.get_nonce(consumer_address)) ######################## # Valid service? if service is None: return jsonify( error=f'This DID has no compute service {did}.'), 400 ######################### # Check privacy privacy_options = service.main.get('privacy', {}) if (algorithm_meta and privacy_options.get('allowRawAlgorithm', True) is False): return jsonify( error=f'cannot run raw algorithm on this did {did}.'), 400 trusted_algorithms = privacy_options.get('trustedAlgorithms', []) if (algorithm_did and trusted_algorithms and algorithm_did not in trusted_algorithms): return jsonify( error=f'cannot run raw algorithm on this did {did}.'), 400 ######################### # Validate ALGORITHM meta if algorithm_meta: algorithm_meta = json.loads(algorithm_meta) if isinstance( algorithm_meta, str) else algorithm_meta algorithm_dict = build_stage_algorithm_dict( consumer_address, algorithm_did, algorithm_token_address, algorithm_tx_id, algorithm_meta, provider_wallet) error_msg, status_code = validate_algorithm_dict( algorithm_dict, algorithm_did) if error_msg: return jsonify(error=error_msg), status_code ######################### # INPUT asset_urls = get_asset_download_urls( asset, provider_wallet, config_file=app.config['CONFIG_FILE']) if not asset_urls: return jsonify(error=f'cannot get url(s) in input did {did}.'), 400 input_dict = dict({'index': 0, 'id': did, 'url': asset_urls}) ######################### # OUTPUT if output_def: output_def = json.loads(output_def) if isinstance( output_def, str) else output_def output_dict = build_stage_output_dict(output_def, asset, consumer_address, provider_wallet) ######################### # STAGE stage = build_stage_dict(input_dict, algorithm_dict, output_dict) ######################### # WORKFLOW workflow = dict({'stages': list([stage])}) # workflow is ready, push it to operator logger.info('Sending: %s', workflow) msg_to_sign = f'{provider_wallet.address}{did}' msg_hash = add_ethereum_prefix_and_hash_msg(msg_to_sign) payload = { 'workflow': workflow, 'providerSignature': Web3Helper.sign_hash(msg_hash, provider_wallet), 'documentId': did, 'agreementId': tx_id, 'owner': consumer_address, 'providerAddress': provider_wallet.address } response = requests_session.post( get_compute_endpoint(), data=json.dumps(payload), headers={'content-type': 'application/json'}) user_nonce.increment_nonce(consumer_address) return Response(response.content, response.status_code, headers={'content-type': 'application/json'}) except InvalidSignatureError as e: msg = f'Consumer signature failed verification: {e}' logger.error(msg, exc_info=1) return jsonify(error=msg), 401 except (ValueError, KeyError, Exception) as e: logger.error(f'Error- {str(e)}', exc_info=1) return jsonify(error=f'Error : {str(e)}'), 500
def download(): """Allows download of asset data file. --- tags: - services consumes: - application/json parameters: - name: consumerAddress in: query description: The consumer address. required: true type: string - name: documentId in: query description: The ID of the asset/document (the DID). required: true type: string - name: url in: query description: This URL is only valid if Provider acts as a proxy. Consumer can't download using the URL if it's not through the Provider. required: true type: string - name: signature in: query description: Signature of the documentId to verify that the consumer has rights to download the asset. - name: index in: query description: Index of the file in the array of files. responses: 200: description: Redirect to valid asset url. 400: description: One of the required attributes is missing. 401: description: Invalid asset data. 500: description: Error """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( # noqa data, 'download', user_nonce=user_nonce, additional_params=["transferTxId", "fileIndex"]) service_id = data.get('serviceId') service_type = data.get('serviceType') signature = data.get('signature') tx_id = data.get("transferTxId") if did.startswith('did:'): did = add_0x_prefix(did_to_id(did)) _tx, _order_log, _transfer_log = validate_order( consumer_address, token_address, float(service.get_cost()), tx_id, did, service_id) validate_transfer_not_used_for_other_service(did, service_id, tx_id, consumer_address, token_address) record_consume_request(did, service_id, tx_id, consumer_address, token_address, service.get_cost()) assert service_type == ServiceTypes.ASSET_ACCESS file_index = int(data.get('fileIndex')) file_attributes = asset.metadata['main']['files'][file_index] content_type = file_attributes.get('contentType', None) url = get_asset_url_at_index(file_index, asset, provider_wallet) download_url = get_download_url(url, app.config['CONFIG_FILE']) logger.info(f'Done processing consume request for asset {did}, ' f' url {download_url}') user_nonce.increment_nonce(consumer_address) return build_download_response(request, requests_session, url, download_url, content_type) except InvalidSignatureError as e: msg = f'Consumer signature failed verification: {e}' logger.error(msg, exc_info=1) return jsonify(error=msg), 401 except Exception as e: logger.error( f'Error: {e}. \n' f'Payload was: documentId={did}, ' f'consumerAddress={consumer_address},' f'signature={signature}' f'serviceId={service_id}' f'serviceType={service_type}', exc_info=1) return jsonify(error=str(e)), 500
def download(): """Allows download of asset data file. --- tags: - services consumes: - application/json parameters: - name: consumerAddress in: query description: The consumer address. required: true type: string - name: documentId in: query description: The ID of the asset/document (the DID). required: true type: string - name: url in: query description: This URL is only valid if Provider acts as a proxy. Consumer can't download using the URL if it's not through the Provider. required: true type: string - name: signature in: query description: Signature of the documentId to verify that the consumer has rights to download the asset. - name: index in: query description: Index of the file in the array of files. responses: 200: description: Redirect to valid asset url. 400: description: One of the required attributes is missing. 401: description: Invalid asset data. 500: description: Error """ data = get_request_data(request) try: ( asset, service, did, consumer_address, token_address, ) = process_consume_request(data) service_id = data.get("serviceId") service_type = data.get("serviceType") tx_id = data.get("transferTxId") if did.startswith("did:"): did = add_0x_prefix(did_to_id(did)) _tx, _order_log, _transfer_log = validate_order( consumer_address, token_address, float(service.get_cost()), tx_id, did, service_id, ) validate_transfer_not_used_for_other_service(did, service_id, tx_id, consumer_address, token_address) record_consume_request(did, service_id, tx_id, consumer_address, token_address, service.get_cost()) assert service_type == ServiceTypes.ASSET_ACCESS file_index = int(data.get("fileIndex")) file_attributes = asset.metadata["main"]["files"][file_index] content_type = file_attributes.get("contentType", None) url = get_asset_url_at_index(file_index, asset, provider_wallet) if not url: return jsonify(error="Cannot decrypt files for this asset."), 400 download_url = get_download_url(url, app.config["CONFIG_FILE"]) logger.info(f"Done processing consume request for asset {did}, " f" url {download_url}") increment_nonce(consumer_address) return build_download_response(request, requests_session, url, download_url, content_type) except Exception as e: logger.error( f"Error: {e}. \n" f"Payload was: documentId={data.get('did')}, " f"consumerAddress={data.get('consumerAddress')}," f"serviceId={data.get('serviceId')}" f"serviceType={data.get('serviceType')}", exc_info=1, ) return jsonify(error=str(e)), 500
def download(): """Allows download of asset data file. --- tags: - services consumes: - application/json parameters: - name: consumerAddress in: query description: The consumer address. required: true type: string - name: serviceAgreementId in: query description: The ID of the service agreement. required: true type: string - name: url in: query description: This URL is only valid if Provider acts as a proxy. Consumer can't download using the URL if it's not through the Provider. required: true type: string - name: signature in: query description: Signature of the documentId to verify that the consumer has rights to download the asset. - name: index in: query description: Index of the file in the array of files. responses: 200: description: Redirect to valid asset url. 400: description: One of the required attributes is missing. 401: description: Invalid asset data. 500: description: Error """ data = get_request_data(request) try: asset, service, did, consumer_address, token_address = process_consume_request( data, 'download', additional_params=["transferTxId", "fileIndex"]) service_id = data.get('serviceId') service_type = data.get('serviceType') signature = data.get('signature') tx_id = data.get("transferTxId") validate_token_transfer( consumer_address, provider_acc.address, token_address, # FIXME: Replace this constant price number 5, #service.get_price(), tx_id) file_index = int(data.get('fileIndex')) file_attributes = asset.metadata['main']['files'][file_index] content_type = file_attributes.get('contentType', None) url = get_asset_url_at_index(file_index, asset, provider_acc) download_url = get_download_url(url, app.config['CONFIG_FILE']) logger.info(f'Done processing consume request for asset {did}, ' f' url {download_url}') return build_download_response(request, requests_session, url, download_url, content_type) except InvalidSignatureError as e: msg = f'Consumer signature failed verification: {e}' logger.error(msg, exc_info=1) return jsonify(error=msg), 401 except Exception as e: logger.error( f'Error: {e}. \n' f'Payload was: documentId={did}, ' f'consumerAddress={consumer_address},' f'signature={signature}' f'serviceId={service_id}' f'serviceType={service_type}', exc_info=1) return jsonify(error=e), 500