mech-client 0.2.21__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,883 @@
1
+ # -*- coding: utf-8 -*-
2
+ # ------------------------------------------------------------------------------
3
+ #
4
+ # Copyright 2024 Valory AG
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # ------------------------------------------------------------------------------
19
+
20
+ """This script allows sending a Request to an on-chain mech marketplace and waiting for the Deliver."""
21
+
22
+
23
+ import asyncio
24
+ import json
25
+ import sys
26
+ import time
27
+ from dataclasses import asdict, make_dataclass
28
+ from datetime import datetime
29
+ from enum import Enum
30
+ from pathlib import Path
31
+ from typing import Any, Dict, Optional, Tuple, cast
32
+
33
+ import requests
34
+ import websocket
35
+ from aea.crypto.base import Crypto
36
+ from aea_ledger_ethereum import EthereumApi, EthereumCrypto
37
+ from eth_utils import to_checksum_address
38
+ from web3.constants import ADDRESS_ZERO
39
+ from web3.contract import Contract as Web3Contract
40
+
41
+ from mech_client.fetch_ipfs_hash import fetch_ipfs_hash
42
+ from mech_client.interact import (
43
+ ConfirmationType,
44
+ MAX_RETRIES,
45
+ MechMarketplaceRequestConfig,
46
+ PRIVATE_KEY_FILE_PATH,
47
+ TIMEOUT,
48
+ WAIT_SLEEP,
49
+ get_contract,
50
+ get_event_signatures,
51
+ get_mech_config,
52
+ )
53
+ from mech_client.prompt_to_ipfs import push_metadata_to_ipfs
54
+ from mech_client.wss import (
55
+ register_event_handlers,
56
+ wait_for_receipt,
57
+ watch_for_marketplace_data_url_from_wss,
58
+ watch_for_marketplace_request_id,
59
+ )
60
+
61
+
62
+ # false positives for [B105:hardcoded_password_string] Possible hardcoded password
63
+ class PaymentType(Enum):
64
+ """Payment type."""
65
+
66
+ NATIVE = "ba699a34be8fe0e7725e93dcbce1701b0211a8ca61330aaeb8a05bf2ec7abed1" # nosec
67
+ TOKEN = "3679d66ef546e66ce9057c4a052f317b135bc8e8c509638f7966edfd4fcf45e9" # nosec
68
+ NATIVE_NVM = (
69
+ "803dd08fe79d91027fc9024e254a0942372b92f3ccabc1bd19f4a5c2b251c316" # nosec
70
+ )
71
+ TOKEN_NVM = (
72
+ "0d6fd99afa9c4c580fab5e341922c2a5c4b61d880da60506193d7bf88944dd14" # nosec
73
+ )
74
+
75
+
76
+ ABI_DIR_PATH = Path(__file__).parent / "abis"
77
+ IMECH_ABI_PATH = ABI_DIR_PATH / "IMech.json"
78
+ ITOKEN_ABI_PATH = ABI_DIR_PATH / "IToken.json"
79
+ IERC1155_ABI_PATH = ABI_DIR_PATH / "IERC1155.json"
80
+ MARKETPLACE_ABI_PATH = ABI_DIR_PATH / "MechMarketplace.json"
81
+
82
+ BALANCE_TRACKER_NATIVE_ABI_PATH = ABI_DIR_PATH / "BalanceTrackerFixedPriceNative.json"
83
+ BALANCE_TRACKER_TOKEN_ABI_PATH = ABI_DIR_PATH / "BalanceTrackerFixedPriceToken.json"
84
+ BALANCE_TRACKER_NVM_NATIVE_ABI_PATH = (
85
+ ABI_DIR_PATH / "BalanceTrackerNvmSubscriptionNative.json"
86
+ )
87
+ BALANCE_TRACKER_NVM_TOKEN_ABI_PATH = (
88
+ ABI_DIR_PATH / "BalanceTrackerNvmSubscriptionToken.json"
89
+ )
90
+
91
+
92
+ PAYMENT_TYPE_TO_ABI_PATH: Dict[str, Path] = {
93
+ PaymentType.NATIVE.value: BALANCE_TRACKER_NATIVE_ABI_PATH,
94
+ PaymentType.TOKEN.value: BALANCE_TRACKER_TOKEN_ABI_PATH,
95
+ PaymentType.NATIVE_NVM.value: BALANCE_TRACKER_NVM_NATIVE_ABI_PATH,
96
+ PaymentType.TOKEN_NVM.value: BALANCE_TRACKER_NVM_TOKEN_ABI_PATH,
97
+ }
98
+
99
+ CHAIN_TO_WRAPPED_TOKEN = {
100
+ 1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
101
+ 10: "0x4200000000000000000000000000000000000006",
102
+ 100: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
103
+ 137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
104
+ 8453: "0x4200000000000000000000000000000000000006",
105
+ 42220: "0x471EcE3750Da237f93B8E339c536989b8978a438",
106
+ }
107
+
108
+
109
+ CHAIN_TO_DEFAULT_MECH_MARKETPLACE_REQUEST_CONFIG = {
110
+ 100: {
111
+ "mech_marketplace_contract": "0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB",
112
+ "priority_mech_address": "0x478ad20eD958dCC5AD4ABa6F4E4cc51e07a840E4",
113
+ "response_timeout": 300,
114
+ "payment_data": "0x",
115
+ },
116
+ 8453: {
117
+ "mech_marketplace_contract": "0xf24eE42edA0fc9b33B7D41B06Ee8ccD2Ef7C5020",
118
+ "priority_mech_address": "0xE183610A420dBD8825fed49C589Fe2d5BFd5b17a",
119
+ "response_timeout": 300,
120
+ "payment_data": "0x",
121
+ },
122
+ }
123
+
124
+
125
+ def fetch_mech_info(
126
+ ledger_api: EthereumApi,
127
+ mech_marketplace_contract: Web3Contract,
128
+ priority_mech_address: str,
129
+ ) -> Tuple[str, int, int, str, Web3Contract]:
130
+ """
131
+ Fetchs the info of the requested mech.
132
+
133
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
134
+ :type ledger_api: EthereumApi
135
+ :param mech_marketplace_contract: The mech marketplace contract instance.
136
+ :type mech_marketplace_contract: Web3Contract
137
+ :param priority_mech_address: Requested mech address
138
+ :type priority_mech_address: str
139
+ :return: The mech info containing payment_type, service_id, max_delivery_rate, mech_payment_balance_tracker and Mech contract.
140
+ :rtype: Tuple[str, int, int, str, Contract]
141
+ """
142
+
143
+ with open(IMECH_ABI_PATH, encoding="utf-8") as f:
144
+ abi = json.load(f)
145
+
146
+ mech_contract = get_contract(
147
+ contract_address=priority_mech_address, abi=abi, ledger_api=ledger_api
148
+ )
149
+ payment_type_bytes = mech_contract.functions.paymentType().call()
150
+ max_delivery_rate = mech_contract.functions.maxDeliveryRate().call()
151
+ service_id = mech_contract.functions.serviceId().call()
152
+ payment_type = payment_type_bytes.hex()
153
+
154
+ mech_payment_balance_tracker = (
155
+ mech_marketplace_contract.functions.mapPaymentTypeBalanceTrackers(
156
+ payment_type_bytes
157
+ ).call()
158
+ )
159
+
160
+ if payment_type not in PaymentType._value2member_map_: # pylint: disable=W0212
161
+ print(" - Invalid mech type detected.")
162
+ sys.exit(1)
163
+
164
+ return (
165
+ payment_type,
166
+ service_id,
167
+ max_delivery_rate,
168
+ mech_payment_balance_tracker,
169
+ mech_contract,
170
+ )
171
+
172
+
173
+ def approve_price_tokens(
174
+ crypto: EthereumCrypto,
175
+ ledger_api: EthereumApi,
176
+ wrapped_token: str,
177
+ mech_payment_balance_tracker: str,
178
+ price: int,
179
+ ) -> str:
180
+ """
181
+ Sends the approve tx for wrapped token of the sender to the requested mech's balance payment tracker contract.
182
+
183
+ :param crypto: The Ethereum crypto object.
184
+ :type crypto: EthereumCrypto
185
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
186
+ :type ledger_api: EthereumApi
187
+ :param wrapped_token: The wrapped token contract address.
188
+ :type wrapped_token: str
189
+ :param mech_payment_balance_tracker: Requested mech's balance tracker contract address
190
+ :type mech_payment_balance_tracker: str
191
+ :param price: Amount of wrapped_token to approve
192
+ :type price: int
193
+ :return: The transaction digest.
194
+ :rtype: str
195
+ """
196
+ sender = crypto.address
197
+
198
+ with open(ITOKEN_ABI_PATH, encoding="utf-8") as f:
199
+ abi = json.load(f)
200
+
201
+ token_contract = get_contract(
202
+ contract_address=wrapped_token, abi=abi, ledger_api=ledger_api
203
+ )
204
+
205
+ user_token_balance = token_contract.functions.balanceOf(sender).call()
206
+ if user_token_balance < price:
207
+ print(
208
+ f" - Sender Token balance low. Needed: {price}, Actual: {user_token_balance}"
209
+ )
210
+ print(f" - Sender Address: {sender}")
211
+ sys.exit(1)
212
+
213
+ tx_args = {"sender_address": sender, "value": 0, "gas": 60000}
214
+ raw_transaction = ledger_api.build_transaction(
215
+ contract_instance=token_contract,
216
+ method_name="approve",
217
+ method_args={"_to": mech_payment_balance_tracker, "_value": price},
218
+ tx_args=tx_args,
219
+ raise_on_try=True,
220
+ )
221
+ signed_transaction = crypto.sign_transaction(raw_transaction)
222
+ transaction_digest = ledger_api.send_signed_transaction(
223
+ signed_transaction,
224
+ raise_on_try=True,
225
+ )
226
+ return transaction_digest
227
+
228
+
229
+ def fetch_requester_nvm_subscription_balance(
230
+ requester: str,
231
+ ledger_api: EthereumApi,
232
+ mech_payment_balance_tracker: str,
233
+ payment_type: str,
234
+ ) -> int:
235
+ """
236
+ Fetches the requester nvm subscription balance.
237
+
238
+ :param requester: The requester's address.
239
+ :type requester: str
240
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
241
+ :type ledger_api: EthereumApi
242
+ :param mech_payment_balance_tracker: Requested mech's balance tracker contract address
243
+ :type mech_payment_balance_tracker: str
244
+ :param payment_type: Requested mech's payment type
245
+ :type payment_type: str
246
+ :return: The requester balance.
247
+ :rtype: int
248
+ """
249
+ with open(
250
+ PAYMENT_TYPE_TO_ABI_PATH[payment_type],
251
+ encoding="utf-8",
252
+ ) as f:
253
+ abi = json.load(f)
254
+
255
+ nvm_balance_tracker_contract = get_contract(
256
+ contract_address=mech_payment_balance_tracker, abi=abi, ledger_api=ledger_api
257
+ )
258
+ subscription_nft_address = (
259
+ nvm_balance_tracker_contract.functions.subscriptionNFT().call()
260
+ )
261
+ subscription_id = (
262
+ nvm_balance_tracker_contract.functions.subscriptionTokenId().call()
263
+ )
264
+
265
+ with open(IERC1155_ABI_PATH, encoding="utf-8") as f:
266
+ abi = json.load(f)
267
+
268
+ subscription_nft_contract = get_contract(
269
+ contract_address=subscription_nft_address, abi=abi, ledger_api=ledger_api
270
+ )
271
+ requester_balance = subscription_nft_contract.functions.balanceOf(
272
+ requester, subscription_id
273
+ ).call()
274
+
275
+ return requester_balance
276
+
277
+
278
+ def send_marketplace_request( # pylint: disable=too-many-arguments,too-many-locals
279
+ crypto: EthereumCrypto,
280
+ ledger_api: EthereumApi,
281
+ marketplace_contract: Web3Contract,
282
+ gas_limit: int,
283
+ prompt: str,
284
+ tool: str,
285
+ method_args_data: MechMarketplaceRequestConfig,
286
+ extra_attributes: Optional[Dict[str, Any]] = None,
287
+ price: int = 10_000_000_000_000_000,
288
+ retries: Optional[int] = None,
289
+ timeout: Optional[float] = None,
290
+ sleep: Optional[float] = None,
291
+ ) -> Optional[str]:
292
+ """
293
+ Sends a request to the mech.
294
+
295
+ :param crypto: The Ethereum crypto object.
296
+ :type crypto: EthereumCrypto
297
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
298
+ :type ledger_api: EthereumApi
299
+ :param marketplace_contract: The mech marketplace contract instance.
300
+ :type marketplace_contract: Web3Contract
301
+ :param gas_limit: Gas limit.
302
+ :type gas_limit: int
303
+ :param prompt: The request prompt.
304
+ :type prompt: str
305
+ :param tool: The requested tool.
306
+ :type tool: str
307
+ :param method_args_data: Method data to use to call the marketplace contract request
308
+ :type method_args_data: MechMarketplaceRequestConfig
309
+ :param extra_attributes: Extra attributes to be included in the request metadata.
310
+ :type extra_attributes: Optional[Dict[str,Any]]
311
+ :param price: The price for the request (default: 10_000_000_000_000_000).
312
+ :type price: int
313
+ :param retries: Number of retries for sending a transaction
314
+ :type retries: int
315
+ :param timeout: Timeout to wait for the transaction
316
+ :type timeout: float
317
+ :param sleep: Amount of sleep before retrying the transaction
318
+ :type sleep: float
319
+ :return: The transaction hash.
320
+ :rtype: Optional[str]
321
+ """
322
+ v1_file_hash_hex_truncated, v1_file_hash_hex = push_metadata_to_ipfs(
323
+ prompt, tool, extra_attributes
324
+ )
325
+ print(
326
+ f" - Prompt uploaded: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}"
327
+ )
328
+ method_name = "request"
329
+ method_args = {
330
+ "requestData": v1_file_hash_hex_truncated,
331
+ "maxDeliveryRate": method_args_data.delivery_rate,
332
+ "paymentType": "0x" + cast(str, method_args_data.payment_type),
333
+ "priorityMech": to_checksum_address(method_args_data.priority_mech_address),
334
+ "responseTimeout": method_args_data.response_timeout,
335
+ "paymentData": method_args_data.payment_data,
336
+ }
337
+ tx_args = {
338
+ "sender_address": crypto.address,
339
+ "value": price,
340
+ "gas": gas_limit,
341
+ }
342
+
343
+ tries = 0
344
+ retries = retries or MAX_RETRIES
345
+ timeout = timeout or TIMEOUT
346
+ sleep = sleep or WAIT_SLEEP
347
+ deadline = datetime.now().timestamp() + timeout
348
+
349
+ while tries < retries and datetime.now().timestamp() < deadline:
350
+ tries += 1
351
+ try:
352
+ raw_transaction = ledger_api.build_transaction(
353
+ contract_instance=marketplace_contract,
354
+ method_name=method_name,
355
+ method_args=method_args,
356
+ tx_args=tx_args,
357
+ raise_on_try=True,
358
+ )
359
+ signed_transaction = crypto.sign_transaction(raw_transaction)
360
+ transaction_digest = ledger_api.send_signed_transaction(
361
+ signed_transaction,
362
+ raise_on_try=True,
363
+ )
364
+ return transaction_digest
365
+ except Exception as e: # pylint: disable=broad-except
366
+ print(
367
+ f"Error occured while sending the transaction: {e}; Retrying in {sleep}"
368
+ )
369
+ time.sleep(sleep)
370
+ return None
371
+
372
+
373
+ def send_offchain_marketplace_request( # pylint: disable=too-many-arguments,too-many-locals
374
+ crypto: EthereumCrypto,
375
+ marketplace_contract: Web3Contract,
376
+ prompt: str,
377
+ tool: str,
378
+ method_args_data: MechMarketplaceRequestConfig,
379
+ extra_attributes: Optional[Dict[str, Any]] = None,
380
+ retries: Optional[int] = None,
381
+ timeout: Optional[float] = None,
382
+ sleep: Optional[float] = None,
383
+ ) -> Optional[Dict]:
384
+ """
385
+ Sends an offchain request to the mech.
386
+
387
+ :param crypto: The Ethereum crypto object.
388
+ :type crypto: EthereumCrypto
389
+ :param marketplace_contract: The mech marketplace contract instance.
390
+ :type marketplace_contract: Web3Contract
391
+ :param prompt: The request prompt.
392
+ :type prompt: str
393
+ :param tool: The requested tool.
394
+ :type tool: str
395
+ :param method_args_data: Method data to use to call the marketplace contract request
396
+ :type method_args_data: MechMarketplaceRequestConfig
397
+ :param extra_attributes: Extra attributes to be included in the request metadata.
398
+ :type extra_attributes: Optional[Dict[str,Any]]
399
+ :param retries: Number of retries for sending a transaction
400
+ :type retries: int
401
+ :param timeout: Timeout to wait for the transaction
402
+ :type timeout: float
403
+ :param sleep: Amount of sleep before retrying the transaction
404
+ :type sleep: float
405
+ :return: The dict containing request info.
406
+ :rtype: Optional[Dict]
407
+ """
408
+ v1_file_hash_hex_truncated, v1_file_hash_hex, ipfs_data = fetch_ipfs_hash(
409
+ prompt, tool, extra_attributes
410
+ )
411
+ print(
412
+ f" - Prompt will shortly be uploaded to: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}"
413
+ )
414
+ method_args = {
415
+ "requestData": v1_file_hash_hex_truncated,
416
+ "maxDeliveryRate": method_args_data.delivery_rate,
417
+ "paymentType": "0x" + cast(str, method_args_data.payment_type),
418
+ "priorityMech": to_checksum_address(method_args_data.priority_mech_address),
419
+ "responseTimeout": method_args_data.response_timeout,
420
+ "paymentData": method_args_data.payment_data,
421
+ }
422
+
423
+ tries = 0
424
+ retries = retries or MAX_RETRIES
425
+ timeout = timeout or TIMEOUT
426
+ sleep = sleep or WAIT_SLEEP
427
+ deadline = datetime.now().timestamp() + timeout
428
+
429
+ while tries < retries and datetime.now().timestamp() < deadline:
430
+ tries += 1
431
+ try:
432
+ nonce = marketplace_contract.functions.mapNonces(crypto.address).call()
433
+ delivery_rate = method_args["maxDeliveryRate"]
434
+ request_id = marketplace_contract.functions.getRequestId(
435
+ method_args["priorityMech"],
436
+ crypto.address,
437
+ method_args["requestData"],
438
+ method_args["maxDeliveryRate"],
439
+ method_args["paymentType"],
440
+ nonce,
441
+ ).call()
442
+ request_id_int = int.from_bytes(request_id, byteorder="big")
443
+ signature = crypto.sign_message(request_id, is_deprecated_mode=True)
444
+
445
+ payload = {
446
+ "sender": crypto.address,
447
+ "signature": signature,
448
+ "ipfs_hash": v1_file_hash_hex_truncated,
449
+ "request_id": request_id_int,
450
+ "delivery_rate": delivery_rate,
451
+ "nonce": nonce,
452
+ "ipfs_data": ipfs_data,
453
+ }
454
+ # @todo changed hardcoded url
455
+ response = requests.post(
456
+ "http://localhost:8000/send_signed_requests",
457
+ data=payload,
458
+ headers={"Content-Type": "application/json"},
459
+ ).json()
460
+ return response
461
+
462
+ except Exception as e: # pylint: disable=broad-except
463
+ print(
464
+ f"Error occured while sending the offchain request: {e}; Retrying in {sleep}"
465
+ )
466
+ time.sleep(sleep)
467
+ return None
468
+
469
+
470
+ def wait_for_marketplace_data_url( # pylint: disable=too-many-arguments, unused-argument
471
+ request_id: str,
472
+ wss: websocket.WebSocket,
473
+ mech_contract: Web3Contract,
474
+ subgraph_url: str,
475
+ deliver_signature: str,
476
+ ledger_api: EthereumApi,
477
+ crypto: Crypto,
478
+ confirmation_type: ConfirmationType = ConfirmationType.WAIT_FOR_BOTH,
479
+ ) -> Any:
480
+ """
481
+ Wait for data from on-chain/off-chain.
482
+
483
+ :param request_id: The ID of the request.
484
+ :type request_id: str
485
+ :param wss: The WebSocket connection object.
486
+ :type wss: websocket.WebSocket
487
+ :param mech_contract: The mech contract instance.
488
+ :type mech_contract: Web3Contract
489
+ :param subgraph_url: Subgraph URL.
490
+ :type subgraph_url: str
491
+ :param deliver_signature: Topic signature for MarketplaceDeliver event
492
+ :type deliver_signature: str
493
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
494
+ :type ledger_api: EthereumApi
495
+ :param crypto: The cryptographic object.
496
+ :type crypto: Crypto
497
+ :param confirmation_type: The confirmation type for the interaction (default: ConfirmationType.WAIT_FOR_BOTH).
498
+ :type confirmation_type: ConfirmationType
499
+ :return: The data received from on-chain/off-chain.
500
+ :rtype: Any
501
+ """
502
+ loop = asyncio.new_event_loop()
503
+ asyncio.set_event_loop(loop)
504
+ tasks = []
505
+
506
+ if confirmation_type in (
507
+ ConfirmationType.OFF_CHAIN,
508
+ ConfirmationType.WAIT_FOR_BOTH,
509
+ ):
510
+ print("Off chain to be implemented")
511
+
512
+ if confirmation_type in (
513
+ ConfirmationType.ON_CHAIN,
514
+ ConfirmationType.WAIT_FOR_BOTH,
515
+ ):
516
+ on_chain_task = loop.create_task(
517
+ watch_for_marketplace_data_url_from_wss(
518
+ request_id=request_id,
519
+ wss=wss,
520
+ mech_contract=mech_contract,
521
+ deliver_signature=deliver_signature,
522
+ ledger_api=ledger_api,
523
+ loop=loop,
524
+ )
525
+ )
526
+ tasks.append(on_chain_task)
527
+
528
+ if subgraph_url:
529
+ print("Subgraph to be implemented")
530
+
531
+ async def _wait_for_tasks() -> Any: # type: ignore
532
+ """Wait for tasks to finish."""
533
+ (finished, *_), unfinished = await asyncio.wait(
534
+ tasks,
535
+ return_when=asyncio.FIRST_COMPLETED,
536
+ )
537
+ for task in unfinished:
538
+ task.cancel()
539
+ if unfinished:
540
+ await asyncio.wait(unfinished)
541
+ return finished.result()
542
+
543
+ result = loop.run_until_complete(_wait_for_tasks())
544
+ return result
545
+
546
+
547
+ def wait_for_offchain_marketplace_data(request_id: str) -> Any:
548
+ """
549
+ Watches for data off-chain on mech.
550
+
551
+ :param request_id: The ID of the request.
552
+ :type request_id: str
553
+ :return: The data returned by the mech.
554
+ :rtype: Any
555
+ """
556
+ while True:
557
+ try:
558
+ # @todo change hardcoded url
559
+ response = requests.get(
560
+ "http://localhost:8000/fetch_offchain_info",
561
+ data={"request_id": request_id},
562
+ ).json()
563
+ if response:
564
+ return response
565
+ except Exception: # pylint: disable=broad-except
566
+ time.sleep(1)
567
+
568
+
569
+ def check_prepaid_balances(
570
+ crypto: Crypto,
571
+ ledger_api: EthereumApi,
572
+ mech_payment_balance_tracker: str,
573
+ payment_type: str,
574
+ max_delivery_rate: int,
575
+ ) -> None:
576
+ """
577
+ Checks the requester's prepaid balances for native and token mech.
578
+
579
+ :param crypto: The cryptographic object.
580
+ :type crypto: Crypto
581
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
582
+ :type ledger_api: EthereumApi
583
+ :param mech_payment_balance_tracker: The mech's balance tracker contract address.
584
+ :type mech_payment_balance_tracker: str
585
+ :param payment_type: The payment type of the mech.
586
+ :type payment_type: str
587
+ :param max_delivery_rate: The max_delivery_rate of the mech
588
+ :type max_delivery_rate: int
589
+ """
590
+ requester = crypto.address
591
+
592
+ if payment_type in [PaymentType.NATIVE.value, PaymentType.TOKEN.value]:
593
+ payment_type_name = PaymentType(payment_type).name.lower()
594
+ payment_type_abi_path = PAYMENT_TYPE_TO_ABI_PATH[payment_type]
595
+
596
+ with open(payment_type_abi_path, encoding="utf-8") as f:
597
+ abi = json.load(f)
598
+
599
+ balance_tracker_contract = get_contract(
600
+ contract_address=mech_payment_balance_tracker,
601
+ abi=abi,
602
+ ledger_api=ledger_api,
603
+ )
604
+ requester_balance = balance_tracker_contract.functions.mapRequesterBalances(
605
+ requester
606
+ ).call()
607
+ if requester_balance < max_delivery_rate:
608
+ print(
609
+ f" - Sender {payment_type_name} deposited balance low. Needed: {max_delivery_rate}, Actual: {requester_balance}"
610
+ )
611
+ print(f" - Sender Address: {requester}")
612
+ print(
613
+ f" - Please use scripts/deposit_{payment_type_name}.py to add balance"
614
+ )
615
+ sys.exit(1)
616
+
617
+
618
+ def marketplace_interact( # pylint: disable=too-many-arguments, too-many-locals, too-many-statements, too-many-return-statements
619
+ prompt: str,
620
+ priority_mech: str,
621
+ use_prepaid: bool = False,
622
+ use_offchain: bool = False,
623
+ tool: str = "",
624
+ extra_attributes: Optional[Dict[str, Any]] = None,
625
+ private_key_path: Optional[str] = None,
626
+ confirmation_type: ConfirmationType = ConfirmationType.WAIT_FOR_BOTH,
627
+ retries: Optional[int] = None,
628
+ timeout: Optional[float] = None,
629
+ sleep: Optional[float] = None,
630
+ chain_config: Optional[str] = None,
631
+ ) -> Any:
632
+ """
633
+ Interact with mech marketplace contract.
634
+
635
+ :param prompt: The interaction prompt.
636
+ :type prompt: str
637
+ :param priority_mech: Priority mech address to use (Optional)
638
+ :type priority_mech: str
639
+ :param use_prepaid: Whether to use prepaid model or not.
640
+ :type use_prepaid: bool
641
+ :param use_offchain: Whether to use offchain model or not.
642
+ :type use_offchain: bool
643
+ :param tool: The tool to interact with (optional).
644
+ :type tool: str
645
+ :param extra_attributes: Extra attributes to be included in the request metadata (optional).
646
+ :type extra_attributes: Optional[Dict[str, Any]]
647
+ :param private_key_path: The path to the private key file (optional).
648
+ :type private_key_path: Optional[str]
649
+ :param confirmation_type: The confirmation type for the interaction (default: ConfirmationType.WAIT_FOR_BOTH).
650
+ :type confirmation_type: ConfirmationType
651
+ :return: The data received from on-chain/off-chain.
652
+ :param retries: Number of retries for sending a transaction
653
+ :type retries: int
654
+ :param timeout: Timeout to wait for the transaction
655
+ :type timeout: float
656
+ :param sleep: Amount of sleep before retrying the transaction
657
+ :type sleep: float
658
+ :param chain_config: Id of the mech's chain configuration (stored configs/mechs.json)
659
+ :type chain_config: str:
660
+ :rtype: Any
661
+ """
662
+
663
+ mech_config = get_mech_config(chain_config)
664
+ ledger_config = mech_config.ledger_config
665
+ priority_mech_address = priority_mech
666
+ mech_marketplace_contract = mech_config.mech_marketplace_contract
667
+ chain_id = ledger_config.chain_id
668
+
669
+ if mech_marketplace_contract == ADDRESS_ZERO:
670
+ print(f"Mech Marketplace not yet supported on {chain_config}")
671
+ return None
672
+
673
+ config_values = CHAIN_TO_DEFAULT_MECH_MARKETPLACE_REQUEST_CONFIG[chain_id].copy()
674
+ if priority_mech_address is not None:
675
+ print("Custom Mech detected")
676
+ config_values.update(
677
+ {
678
+ "priority_mech_address": priority_mech_address,
679
+ "mech_marketplace_contract": mech_marketplace_contract,
680
+ }
681
+ )
682
+
683
+ mech_marketplace_request_config: MechMarketplaceRequestConfig = make_dataclass(
684
+ "MechMarketplaceRequestConfig",
685
+ ((k, type(v)) for k, v in config_values.items()),
686
+ )(**config_values)
687
+
688
+ contract_address = cast(
689
+ str, mech_marketplace_request_config.mech_marketplace_contract
690
+ )
691
+
692
+ private_key_path = private_key_path or PRIVATE_KEY_FILE_PATH
693
+ if not Path(private_key_path).exists():
694
+ raise FileNotFoundError(
695
+ f"Private key file `{private_key_path}` does not exist!"
696
+ )
697
+
698
+ wss = websocket.create_connection(mech_config.wss_endpoint)
699
+ crypto = EthereumCrypto(private_key_path=private_key_path)
700
+ ledger_api = EthereumApi(**asdict(ledger_config))
701
+
702
+ with open(MARKETPLACE_ABI_PATH, encoding="utf-8") as f:
703
+ abi = json.load(f)
704
+
705
+ mech_marketplace_contract = get_contract(
706
+ contract_address=contract_address, abi=abi, ledger_api=ledger_api
707
+ )
708
+
709
+ print("Fetching Mech Info...")
710
+ priority_mech_address = cast(
711
+ str, mech_marketplace_request_config.priority_mech_address
712
+ )
713
+ (
714
+ payment_type,
715
+ _,
716
+ max_delivery_rate,
717
+ mech_payment_balance_tracker,
718
+ mech_contract,
719
+ ) = fetch_mech_info(
720
+ ledger_api,
721
+ mech_marketplace_contract,
722
+ priority_mech_address,
723
+ )
724
+ mech_marketplace_request_config.delivery_rate = max_delivery_rate
725
+ mech_marketplace_request_config.payment_type = payment_type
726
+
727
+ with open(IMECH_ABI_PATH, encoding="utf-8") as f:
728
+ abi = json.load(f)
729
+
730
+ (
731
+ marketplace_request_event_signature,
732
+ marketplace_deliver_event_signature,
733
+ ) = get_event_signatures(abi=abi)
734
+ register_event_handlers(
735
+ wss=wss,
736
+ contract_address=priority_mech_address,
737
+ crypto=crypto,
738
+ request_signature=marketplace_request_event_signature,
739
+ deliver_signature=marketplace_deliver_event_signature,
740
+ )
741
+
742
+ if not use_prepaid:
743
+ price = max_delivery_rate
744
+ if payment_type == PaymentType.TOKEN.value:
745
+ print("Token Mech detected, approving wrapped token for price payment...")
746
+ wxdai = CHAIN_TO_WRAPPED_TOKEN[chain_id]
747
+ approve_tx = approve_price_tokens(
748
+ crypto, ledger_api, wxdai, mech_payment_balance_tracker, price
749
+ )
750
+ if not approve_tx:
751
+ print("Unable to approve allowance")
752
+ return None
753
+
754
+ transaction_url_formatted = mech_config.transaction_url.format(
755
+ transaction_digest=approve_tx
756
+ )
757
+ print(f" - Transaction sent: {transaction_url_formatted}")
758
+ print(" - Waiting for transaction receipt...")
759
+ wait_for_receipt(approve_tx, ledger_api)
760
+ # set price 0 to not send any msg.value in request transaction for token type mech
761
+ price = 0
762
+
763
+ else:
764
+ print("Prepaid request to be used, skipping payment")
765
+ price = 0
766
+
767
+ check_prepaid_balances(
768
+ crypto,
769
+ ledger_api,
770
+ mech_payment_balance_tracker,
771
+ payment_type,
772
+ max_delivery_rate,
773
+ )
774
+
775
+ if payment_type in [PaymentType.NATIVE_NVM.value, PaymentType.TOKEN_NVM.value]:
776
+ nvm_mech_type = PaymentType(payment_type).name.lower()
777
+ print(
778
+ f"{nvm_mech_type} Nevermined Mech detected, subscription credits to be used"
779
+ )
780
+ requester = crypto.address
781
+ requester_balance = fetch_requester_nvm_subscription_balance(
782
+ requester, ledger_api, mech_payment_balance_tracker, payment_type
783
+ )
784
+ if requester_balance < price:
785
+ print(
786
+ f" - Sender Subscription balance low. Needed: {price}, Actual: {requester_balance}"
787
+ )
788
+ print(f" - Sender Address: {requester}")
789
+ sys.exit(1)
790
+
791
+ # set price 0 to not send any msg.value in request transaction for nvm type mech
792
+ price = 0
793
+
794
+ if not use_offchain:
795
+ print("Sending Mech Marketplace request...")
796
+ transaction_digest = send_marketplace_request(
797
+ crypto=crypto,
798
+ ledger_api=ledger_api,
799
+ marketplace_contract=mech_marketplace_contract,
800
+ gas_limit=mech_config.gas_limit,
801
+ price=price,
802
+ prompt=prompt,
803
+ tool=tool,
804
+ method_args_data=mech_marketplace_request_config,
805
+ extra_attributes=extra_attributes,
806
+ retries=retries,
807
+ timeout=timeout,
808
+ sleep=sleep,
809
+ )
810
+
811
+ if not transaction_digest:
812
+ print("Unable to send request")
813
+ return None
814
+
815
+ transaction_url_formatted = mech_config.transaction_url.format(
816
+ transaction_digest=transaction_digest
817
+ )
818
+ print(f" - Transaction sent: {transaction_url_formatted}")
819
+ print(" - Waiting for transaction receipt...")
820
+
821
+ request_id = watch_for_marketplace_request_id(
822
+ marketplace_contract=mech_marketplace_contract,
823
+ ledger_api=ledger_api,
824
+ tx_hash=transaction_digest,
825
+ )
826
+ request_id_int = int.from_bytes(bytes.fromhex(request_id), byteorder="big")
827
+ print(f" - Created on-chain request with ID {request_id_int}")
828
+ print("")
829
+
830
+ data_url = wait_for_marketplace_data_url(
831
+ request_id=request_id,
832
+ wss=wss,
833
+ mech_contract=mech_contract,
834
+ subgraph_url=mech_config.subgraph_url,
835
+ deliver_signature=marketplace_deliver_event_signature,
836
+ ledger_api=ledger_api,
837
+ crypto=crypto,
838
+ confirmation_type=confirmation_type,
839
+ )
840
+
841
+ if data_url:
842
+ print(f" - Data arrived: {data_url}")
843
+ data = requests.get(f"{data_url}/{request_id_int}", timeout=30).json()
844
+ print(" - Data from agent:")
845
+ print(json.dumps(data, indent=2))
846
+ return data
847
+ return None
848
+
849
+ print("Sending Offchain Mech Marketplace request...")
850
+ response = send_offchain_marketplace_request(
851
+ crypto=crypto,
852
+ marketplace_contract=mech_marketplace_contract,
853
+ prompt=prompt,
854
+ tool=tool,
855
+ method_args_data=mech_marketplace_request_config,
856
+ extra_attributes=extra_attributes,
857
+ retries=retries,
858
+ timeout=timeout,
859
+ sleep=sleep,
860
+ )
861
+
862
+ if not response:
863
+ return None
864
+
865
+ request_id = response["request_id"]
866
+ print(f" - Created off-chain request with ID {request_id}")
867
+ print("")
868
+
869
+ # @note as we are directly querying data from done task list, we get the full data instead of the ipfs hash
870
+ print("Waiting for Offchain Mech Marketplace deliver...")
871
+ data = wait_for_offchain_marketplace_data(
872
+ request_id=request_id,
873
+ )
874
+
875
+ if data:
876
+ task_result = data["task_result"]
877
+ data_url = f"https://gateway.autonolas.tech/ipfs/f01701220{task_result}"
878
+ print(f" - Data arrived: {data_url}")
879
+ data = requests.get(f"{data_url}/{request_id}", timeout=30).json()
880
+ print(" - Data from agent:")
881
+ print(json.dumps(data, indent=2))
882
+ return data
883
+ return None