mech-client 0.2.21__py3-none-any.whl → 0.3.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,880 @@
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 pathlib import Path
30
+ from typing import Any, Dict, List, Optional, Tuple, cast
31
+
32
+ import requests
33
+ import websocket
34
+ from aea.crypto.base import Crypto
35
+ from aea_ledger_ethereum import EthereumApi, EthereumCrypto
36
+ from eth_utils import to_checksum_address
37
+ from web3.constants import ADDRESS_ZERO
38
+ from web3.contract import Contract as Web3Contract
39
+
40
+ from mech_client.fetch_ipfs_hash import fetch_ipfs_hash
41
+ from mech_client.interact import (
42
+ ConfirmationType,
43
+ MAX_RETRIES,
44
+ MechMarketplaceRequestConfig,
45
+ PRIVATE_KEY_FILE_PATH,
46
+ TIMEOUT,
47
+ WAIT_SLEEP,
48
+ calculate_topic_id,
49
+ get_contract,
50
+ get_mech_config,
51
+ )
52
+ from mech_client.prompt_to_ipfs import push_metadata_to_ipfs
53
+ from mech_client.wss import (
54
+ register_event_handlers,
55
+ wait_for_receipt,
56
+ watch_for_marketplace_data_url_from_wss,
57
+ watch_for_marketplace_request_id,
58
+ )
59
+
60
+
61
+ # false positives for [B105:hardcoded_password_string] Possible hardcoded password
62
+ PAYMENT_TYPE_NATIVE = (
63
+ "ba699a34be8fe0e7725e93dcbce1701b0211a8ca61330aaeb8a05bf2ec7abed1" # nosec
64
+ )
65
+ PAYMENT_TYPE_TOKEN = (
66
+ "3679d66ef546e66ce9057c4a052f317b135bc8e8c509638f7966edfd4fcf45e9" # nosec
67
+ )
68
+ PAYMENT_TYPE_NVM = (
69
+ "803dd08fe79d91027fc9024e254a0942372b92f3ccabc1bd19f4a5c2b251c316" # nosec
70
+ )
71
+
72
+ CHAIN_TO_WRAPPED_TOKEN = {
73
+ 1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
74
+ 10: "0x4200000000000000000000000000000000000006",
75
+ 100: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
76
+ 137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
77
+ 8453: "0x4200000000000000000000000000000000000006",
78
+ 42220: "0x471EcE3750Da237f93B8E339c536989b8978a438",
79
+ }
80
+
81
+
82
+ CHAIN_TO_DEFAULT_MECH_MARKETPLACE_REQUEST_CONFIG = {
83
+ 100: {
84
+ "mech_marketplace_contract": "0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB",
85
+ "priority_mech_address": "0x478ad20eD958dCC5AD4ABa6F4E4cc51e07a840E4",
86
+ "response_timeout": 300,
87
+ "payment_data": "0x",
88
+ }
89
+ }
90
+
91
+
92
+ def get_event_signatures(abi: List) -> Tuple[str, str]:
93
+ """Calculate `Marketplace Request` and `Marketplace Deliver` event topics"""
94
+ marketplace_request, marketplace_deliver = "", ""
95
+ for obj in abi:
96
+ if obj["type"] != "event":
97
+ continue
98
+ if obj["name"] == "MarketplaceDeliver":
99
+ marketplace_deliver = calculate_topic_id(event=obj)
100
+ if obj["name"] == "MarketplaceRequest":
101
+ marketplace_request = calculate_topic_id(event=obj)
102
+ return marketplace_request, marketplace_deliver
103
+
104
+
105
+ def fetch_mech_info(
106
+ ledger_api: EthereumApi,
107
+ mech_marketplace_contract: Web3Contract,
108
+ priority_mech_address: str,
109
+ ) -> Tuple[str, int, int, str, Web3Contract]:
110
+ """
111
+ Fetchs the info of the requested mech.
112
+
113
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
114
+ :type ledger_api: EthereumApi
115
+ :param mech_marketplace_contract: The mech marketplace contract instance.
116
+ :type mech_marketplace_contract: Web3Contract
117
+ :param priority_mech_address: Requested mech address
118
+ :type priority_mech_address: str
119
+ :return: The mech info containing payment_type, service_id, max_delivery_rate, mech_payment_balance_tracker and Mech contract.
120
+ :rtype: Tuple[str, int, int, str, Contract]
121
+ """
122
+
123
+ with open(Path(__file__).parent / "abis" / "IMech.json", encoding="utf-8") as f:
124
+ abi = json.load(f)
125
+
126
+ mech_contract = get_contract(
127
+ contract_address=priority_mech_address, abi=abi, ledger_api=ledger_api
128
+ )
129
+ payment_type_bytes = mech_contract.functions.paymentType().call()
130
+ max_delivery_rate = mech_contract.functions.maxDeliveryRate().call()
131
+ service_id = mech_contract.functions.serviceId().call()
132
+ payment_type = payment_type_bytes.hex()
133
+
134
+ mech_payment_balance_tracker = (
135
+ mech_marketplace_contract.functions.mapPaymentTypeBalanceTrackers(
136
+ payment_type_bytes
137
+ ).call()
138
+ )
139
+
140
+ if payment_type not in [PAYMENT_TYPE_NATIVE, PAYMENT_TYPE_TOKEN, PAYMENT_TYPE_NVM]:
141
+ print(" - Invalid mech type detected.")
142
+ sys.exit(1)
143
+
144
+ return (
145
+ payment_type,
146
+ service_id,
147
+ max_delivery_rate,
148
+ mech_payment_balance_tracker,
149
+ mech_contract,
150
+ )
151
+
152
+
153
+ def approve_price_tokens(
154
+ crypto: EthereumCrypto,
155
+ ledger_api: EthereumApi,
156
+ wrapped_token: str,
157
+ mech_payment_balance_tracker: str,
158
+ price: int,
159
+ ) -> str:
160
+ """
161
+ Sends the approve tx for wrapped token of the sender to the requested mech's balance payment tracker contract.
162
+
163
+ :param crypto: The Ethereum crypto object.
164
+ :type crypto: EthereumCrypto
165
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
166
+ :type ledger_api: EthereumApi
167
+ :param wrapped_token: The wrapped token contract address.
168
+ :type wrapped_token: str
169
+ :param mech_payment_balance_tracker: Requested mech's balance tracker contract address
170
+ :type mech_payment_balance_tracker: str
171
+ :param price: Amount of wrapped_token to approve
172
+ :type price: int
173
+ :return: The transaction digest.
174
+ :rtype: str
175
+ """
176
+ sender = crypto.address
177
+
178
+ with open(Path(__file__).parent / "abis" / "IToken.json", encoding="utf-8") as f:
179
+ abi = json.load(f)
180
+
181
+ token_contract = get_contract(
182
+ contract_address=wrapped_token, abi=abi, ledger_api=ledger_api
183
+ )
184
+
185
+ user_token_balance = token_contract.functions.balanceOf(sender).call()
186
+ if user_token_balance < price:
187
+ print(
188
+ f" - Sender Token balance low. Needed: {price}, Actual: {user_token_balance}"
189
+ )
190
+ print(f" - Sender Address: {sender}")
191
+ sys.exit(1)
192
+
193
+ tx_args = {"sender_address": sender, "value": 0, "gas": 60000}
194
+ raw_transaction = ledger_api.build_transaction(
195
+ contract_instance=token_contract,
196
+ method_name="approve",
197
+ method_args={"_to": mech_payment_balance_tracker, "_value": price},
198
+ tx_args=tx_args,
199
+ raise_on_try=True,
200
+ )
201
+ signed_transaction = crypto.sign_transaction(raw_transaction)
202
+ transaction_digest = ledger_api.send_signed_transaction(
203
+ signed_transaction,
204
+ raise_on_try=True,
205
+ )
206
+ return transaction_digest
207
+
208
+
209
+ def fetch_requester_nvm_subscription_balance(
210
+ requester: str,
211
+ ledger_api: EthereumApi,
212
+ mech_payment_balance_tracker: str,
213
+ ) -> int:
214
+ """
215
+ Fetches the requester nvm subscription balance.
216
+
217
+ :param requester: The requester's address.
218
+ :type requester: str
219
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
220
+ :type ledger_api: EthereumApi
221
+ :param mech_payment_balance_tracker: Requested mech's balance tracker contract address
222
+ :type mech_payment_balance_tracker: str
223
+ :return: The requester balance.
224
+ :rtype: int
225
+ """
226
+ with open(
227
+ Path(__file__).parent / "abis" / "BalanceTrackerNvmSubscriptionNative.json",
228
+ encoding="utf-8",
229
+ ) as f:
230
+ abi = json.load(f)
231
+
232
+ nvm_balance_tracker_contract = get_contract(
233
+ contract_address=mech_payment_balance_tracker, abi=abi, ledger_api=ledger_api
234
+ )
235
+ subscription_nft_address = (
236
+ nvm_balance_tracker_contract.functions.subscriptionNFT().call()
237
+ )
238
+ subscription_id = (
239
+ nvm_balance_tracker_contract.functions.subscriptionTokenId().call()
240
+ )
241
+
242
+ with open(
243
+ Path(__file__).parent / "abis" / "IERC1155.json",
244
+ encoding="utf-8",
245
+ ) as f:
246
+ abi = json.load(f)
247
+
248
+ subscription_nft_contract = get_contract(
249
+ contract_address=subscription_nft_address, abi=abi, ledger_api=ledger_api
250
+ )
251
+ requester_balance = subscription_nft_contract.functions.balanceOf(
252
+ requester, subscription_id
253
+ ).call()
254
+
255
+ return requester_balance
256
+
257
+
258
+ def send_marketplace_request( # pylint: disable=too-many-arguments,too-many-locals
259
+ crypto: EthereumCrypto,
260
+ ledger_api: EthereumApi,
261
+ marketplace_contract: Web3Contract,
262
+ gas_limit: int,
263
+ prompt: str,
264
+ tool: str,
265
+ method_args_data: MechMarketplaceRequestConfig,
266
+ extra_attributes: Optional[Dict[str, Any]] = None,
267
+ price: int = 10_000_000_000_000_000,
268
+ retries: Optional[int] = None,
269
+ timeout: Optional[float] = None,
270
+ sleep: Optional[float] = None,
271
+ ) -> Optional[str]:
272
+ """
273
+ Sends a request to the mech.
274
+
275
+ :param crypto: The Ethereum crypto object.
276
+ :type crypto: EthereumCrypto
277
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
278
+ :type ledger_api: EthereumApi
279
+ :param marketplace_contract: The mech marketplace contract instance.
280
+ :type marketplace_contract: Web3Contract
281
+ :param gas_limit: Gas limit.
282
+ :type gas_limit: int
283
+ :param prompt: The request prompt.
284
+ :type prompt: str
285
+ :param tool: The requested tool.
286
+ :type tool: str
287
+ :param method_args_data: Method data to use to call the marketplace contract request
288
+ :type method_args_data: MechMarketplaceRequestConfig
289
+ :param extra_attributes: Extra attributes to be included in the request metadata.
290
+ :type extra_attributes: Optional[Dict[str,Any]]
291
+ :param price: The price for the request (default: 10_000_000_000_000_000).
292
+ :type price: int
293
+ :param retries: Number of retries for sending a transaction
294
+ :type retries: int
295
+ :param timeout: Timeout to wait for the transaction
296
+ :type timeout: float
297
+ :param sleep: Amount of sleep before retrying the transaction
298
+ :type sleep: float
299
+ :return: The transaction hash.
300
+ :rtype: Optional[str]
301
+ """
302
+ v1_file_hash_hex_truncated, v1_file_hash_hex = push_metadata_to_ipfs(
303
+ prompt, tool, extra_attributes
304
+ )
305
+ print(
306
+ f" - Prompt uploaded: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}"
307
+ )
308
+ method_name = "request"
309
+ method_args = {
310
+ "requestData": v1_file_hash_hex_truncated,
311
+ "maxDeliveryRate": method_args_data.delivery_rate,
312
+ "paymentType": "0x" + cast(str, method_args_data.payment_type),
313
+ "priorityMech": to_checksum_address(method_args_data.priority_mech_address),
314
+ "responseTimeout": method_args_data.response_timeout,
315
+ "paymentData": method_args_data.payment_data,
316
+ }
317
+ tx_args = {
318
+ "sender_address": crypto.address,
319
+ "value": price,
320
+ "gas": gas_limit,
321
+ }
322
+
323
+ tries = 0
324
+ retries = retries or MAX_RETRIES
325
+ timeout = timeout or TIMEOUT
326
+ sleep = sleep or WAIT_SLEEP
327
+ deadline = datetime.now().timestamp() + timeout
328
+
329
+ while tries < retries and datetime.now().timestamp() < deadline:
330
+ tries += 1
331
+ try:
332
+ raw_transaction = ledger_api.build_transaction(
333
+ contract_instance=marketplace_contract,
334
+ method_name=method_name,
335
+ method_args=method_args,
336
+ tx_args=tx_args,
337
+ raise_on_try=True,
338
+ )
339
+ signed_transaction = crypto.sign_transaction(raw_transaction)
340
+ transaction_digest = ledger_api.send_signed_transaction(
341
+ signed_transaction,
342
+ raise_on_try=True,
343
+ )
344
+ return transaction_digest
345
+ except Exception as e: # pylint: disable=broad-except
346
+ print(
347
+ f"Error occured while sending the transaction: {e}; Retrying in {sleep}"
348
+ )
349
+ time.sleep(sleep)
350
+ return None
351
+
352
+
353
+ def send_offchain_marketplace_request( # pylint: disable=too-many-arguments,too-many-locals
354
+ crypto: EthereumCrypto,
355
+ marketplace_contract: Web3Contract,
356
+ prompt: str,
357
+ tool: str,
358
+ method_args_data: MechMarketplaceRequestConfig,
359
+ extra_attributes: Optional[Dict[str, Any]] = None,
360
+ retries: Optional[int] = None,
361
+ timeout: Optional[float] = None,
362
+ sleep: Optional[float] = None,
363
+ ) -> Optional[Dict]:
364
+ """
365
+ Sends an offchain request to the mech.
366
+
367
+ :param crypto: The Ethereum crypto object.
368
+ :type crypto: EthereumCrypto
369
+ :param marketplace_contract: The mech marketplace contract instance.
370
+ :type marketplace_contract: Web3Contract
371
+ :param prompt: The request prompt.
372
+ :type prompt: str
373
+ :param tool: The requested tool.
374
+ :type tool: str
375
+ :param method_args_data: Method data to use to call the marketplace contract request
376
+ :type method_args_data: MechMarketplaceRequestConfig
377
+ :param extra_attributes: Extra attributes to be included in the request metadata.
378
+ :type extra_attributes: Optional[Dict[str,Any]]
379
+ :param retries: Number of retries for sending a transaction
380
+ :type retries: int
381
+ :param timeout: Timeout to wait for the transaction
382
+ :type timeout: float
383
+ :param sleep: Amount of sleep before retrying the transaction
384
+ :type sleep: float
385
+ :return: The dict containing request info.
386
+ :rtype: Optional[Dict]
387
+ """
388
+ v1_file_hash_hex_truncated, v1_file_hash_hex, ipfs_data = fetch_ipfs_hash(
389
+ prompt, tool, extra_attributes
390
+ )
391
+ print(
392
+ f" - Prompt will shortly be uploaded to: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}"
393
+ )
394
+ method_args = {
395
+ "requestData": v1_file_hash_hex_truncated,
396
+ "maxDeliveryRate": method_args_data.delivery_rate,
397
+ "paymentType": "0x" + cast(str, method_args_data.payment_type),
398
+ "priorityMech": to_checksum_address(method_args_data.priority_mech_address),
399
+ "responseTimeout": method_args_data.response_timeout,
400
+ "paymentData": method_args_data.payment_data,
401
+ }
402
+
403
+ tries = 0
404
+ retries = retries or MAX_RETRIES
405
+ timeout = timeout or TIMEOUT
406
+ sleep = sleep or WAIT_SLEEP
407
+ deadline = datetime.now().timestamp() + timeout
408
+
409
+ while tries < retries and datetime.now().timestamp() < deadline:
410
+ tries += 1
411
+ try:
412
+ nonce = marketplace_contract.functions.mapNonces(crypto.address).call()
413
+ delivery_rate = method_args["maxDeliveryRate"]
414
+ request_id = marketplace_contract.functions.getRequestId(
415
+ method_args["priorityMech"],
416
+ crypto.address,
417
+ method_args["requestData"],
418
+ method_args["maxDeliveryRate"],
419
+ method_args["paymentType"],
420
+ nonce,
421
+ ).call()
422
+ request_id_int = int.from_bytes(request_id, byteorder="big")
423
+ signature = crypto.sign_message(request_id, is_deprecated_mode=True)
424
+
425
+ payload = {
426
+ "sender": crypto.address,
427
+ "signature": signature,
428
+ "ipfs_hash": v1_file_hash_hex_truncated,
429
+ "request_id": request_id_int,
430
+ "delivery_rate": delivery_rate,
431
+ "nonce": nonce,
432
+ "ipfs_data": ipfs_data,
433
+ }
434
+ # @todo changed hardcoded url
435
+ response = requests.post(
436
+ "http://localhost:8000/send_signed_requests",
437
+ data=payload,
438
+ headers={"Content-Type": "application/json"},
439
+ ).json()
440
+ return response
441
+
442
+ except Exception as e: # pylint: disable=broad-except
443
+ print(
444
+ f"Error occured while sending the offchain request: {e}; Retrying in {sleep}"
445
+ )
446
+ time.sleep(sleep)
447
+ return None
448
+
449
+
450
+ def wait_for_marketplace_data_url( # pylint: disable=too-many-arguments, unused-argument
451
+ request_id: str,
452
+ wss: websocket.WebSocket,
453
+ mech_contract: Web3Contract,
454
+ subgraph_url: str,
455
+ deliver_signature: str,
456
+ ledger_api: EthereumApi,
457
+ crypto: Crypto,
458
+ confirmation_type: ConfirmationType = ConfirmationType.WAIT_FOR_BOTH,
459
+ ) -> Any:
460
+ """
461
+ Wait for data from on-chain/off-chain.
462
+
463
+ :param request_id: The ID of the request.
464
+ :type request_id: str
465
+ :param wss: The WebSocket connection object.
466
+ :type wss: websocket.WebSocket
467
+ :param mech_contract: The mech contract instance.
468
+ :type mech_contract: Web3Contract
469
+ :param subgraph_url: Subgraph URL.
470
+ :type subgraph_url: str
471
+ :param deliver_signature: Topic signature for MarketplaceDeliver event
472
+ :type deliver_signature: str
473
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
474
+ :type ledger_api: EthereumApi
475
+ :param crypto: The cryptographic object.
476
+ :type crypto: Crypto
477
+ :param confirmation_type: The confirmation type for the interaction (default: ConfirmationType.WAIT_FOR_BOTH).
478
+ :type confirmation_type: ConfirmationType
479
+ :return: The data received from on-chain/off-chain.
480
+ :rtype: Any
481
+ """
482
+ loop = asyncio.new_event_loop()
483
+ asyncio.set_event_loop(loop)
484
+ tasks = []
485
+
486
+ if confirmation_type in (
487
+ ConfirmationType.OFF_CHAIN,
488
+ ConfirmationType.WAIT_FOR_BOTH,
489
+ ):
490
+ print("Off chain to be implemented")
491
+
492
+ if confirmation_type in (
493
+ ConfirmationType.ON_CHAIN,
494
+ ConfirmationType.WAIT_FOR_BOTH,
495
+ ):
496
+ on_chain_task = loop.create_task(
497
+ watch_for_marketplace_data_url_from_wss(
498
+ request_id=request_id,
499
+ wss=wss,
500
+ mech_contract=mech_contract,
501
+ deliver_signature=deliver_signature,
502
+ ledger_api=ledger_api,
503
+ loop=loop,
504
+ )
505
+ )
506
+ tasks.append(on_chain_task)
507
+
508
+ if subgraph_url:
509
+ print("Subgraph to be implemented")
510
+
511
+ async def _wait_for_tasks() -> Any: # type: ignore
512
+ """Wait for tasks to finish."""
513
+ (finished, *_), unfinished = await asyncio.wait(
514
+ tasks,
515
+ return_when=asyncio.FIRST_COMPLETED,
516
+ )
517
+ for task in unfinished:
518
+ task.cancel()
519
+ if unfinished:
520
+ await asyncio.wait(unfinished)
521
+ return finished.result()
522
+
523
+ result = loop.run_until_complete(_wait_for_tasks())
524
+ return result
525
+
526
+
527
+ def wait_for_offchain_marketplace_data(request_id: str) -> Any:
528
+ """
529
+ Watches for data off-chain on mech.
530
+
531
+ :param request_id: The ID of the request.
532
+ :type request_id: str
533
+ :return: The data returned by the mech.
534
+ :rtype: Any
535
+ """
536
+ while True:
537
+ try:
538
+ # @todo change hardcoded url
539
+ response = requests.get(
540
+ "http://localhost:8000/fetch_offchain_info",
541
+ data={"request_id": request_id},
542
+ ).json()
543
+ if response:
544
+ return response
545
+ except Exception: # pylint: disable=broad-except
546
+ time.sleep(1)
547
+
548
+
549
+ def check_prepaid_balances(
550
+ crypto: Crypto,
551
+ ledger_api: EthereumApi,
552
+ mech_payment_balance_tracker: str,
553
+ payment_type: str,
554
+ max_delivery_rate: int,
555
+ ) -> None:
556
+ """
557
+ Checks the requester's prepaid balances for native and token mech.
558
+
559
+ :param crypto: The cryptographic object.
560
+ :type crypto: Crypto
561
+ :param ledger_api: The Ethereum API used for interacting with the ledger.
562
+ :type ledger_api: EthereumApi
563
+ :param mech_payment_balance_tracker: The mech's balance tracker contract address.
564
+ :type mech_payment_balance_tracker: str
565
+ :param payment_type: The payment type of the mech.
566
+ :type payment_type: str
567
+ :param max_delivery_rate: The max_delivery_rate of the mech
568
+ :type max_delivery_rate: int
569
+ """
570
+ requester = crypto.address
571
+ if payment_type == PAYMENT_TYPE_NATIVE:
572
+ with open(
573
+ Path(__file__).parent / "abis" / "BalanceTrackerFixedPriceNative.json",
574
+ encoding="utf-8",
575
+ ) as f:
576
+ abi = json.load(f)
577
+
578
+ balance_tracker_contract = get_contract(
579
+ contract_address=mech_payment_balance_tracker,
580
+ abi=abi,
581
+ ledger_api=ledger_api,
582
+ )
583
+ requester_balance = balance_tracker_contract.functions.mapRequesterBalances(
584
+ requester
585
+ ).call()
586
+ if requester_balance < max_delivery_rate:
587
+ print(
588
+ f" - Sender Native deposited balance low. Needed: {max_delivery_rate}, Actual: {requester_balance}"
589
+ )
590
+ print(f" - Sender Address: {requester}")
591
+ print(" - Please use scripts/deposit_native.py to add balance")
592
+ sys.exit(1)
593
+
594
+ if payment_type == PAYMENT_TYPE_TOKEN:
595
+ with open(
596
+ Path(__file__).parent / "abis" / "BalanceTrackerFixedPriceToken.json",
597
+ encoding="utf-8",
598
+ ) as f:
599
+ abi = json.load(f)
600
+
601
+ balance_tracker_contract = get_contract(
602
+ contract_address=mech_payment_balance_tracker,
603
+ abi=abi,
604
+ ledger_api=ledger_api,
605
+ )
606
+ requester_balance = balance_tracker_contract.functions.mapRequesterBalances(
607
+ requester
608
+ ).call()
609
+ if requester_balance < max_delivery_rate:
610
+ print(
611
+ f" - Sender Token deposited balance low. Needed: {max_delivery_rate}, Actual: {requester_balance}"
612
+ )
613
+ print(f" - Sender Address: {requester}")
614
+ print(" - Please use scripts/deposit_token.py to add balance")
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(
703
+ Path(__file__).parent / "abis" / "MechMarketplace.json", encoding="utf-8"
704
+ ) as f:
705
+ abi = json.load(f)
706
+
707
+ mech_marketplace_contract = get_contract(
708
+ contract_address=contract_address, abi=abi, ledger_api=ledger_api
709
+ )
710
+
711
+ print("Fetching Mech Info...")
712
+ priority_mech_address = cast(
713
+ str, mech_marketplace_request_config.priority_mech_address
714
+ )
715
+ (
716
+ payment_type,
717
+ _,
718
+ max_delivery_rate,
719
+ mech_payment_balance_tracker,
720
+ mech_contract,
721
+ ) = fetch_mech_info(
722
+ ledger_api,
723
+ mech_marketplace_contract,
724
+ priority_mech_address,
725
+ )
726
+ mech_marketplace_request_config.delivery_rate = max_delivery_rate
727
+ mech_marketplace_request_config.payment_type = payment_type
728
+
729
+ (
730
+ marketplace_request_event_signature,
731
+ marketplace_deliver_event_signature,
732
+ ) = get_event_signatures(abi=abi)
733
+
734
+ register_event_handlers(
735
+ wss=wss,
736
+ contract_address=contract_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 == PAYMENT_TYPE_TOKEN:
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 == PAYMENT_TYPE_NVM:
776
+ print("Nevermined Mech detected, subscription credits to be used")
777
+ requester = crypto.address
778
+ requester_balance = fetch_requester_nvm_subscription_balance(
779
+ requester, ledger_api, mech_payment_balance_tracker
780
+ )
781
+ if requester_balance < price:
782
+ print(
783
+ f" - Sender Subscription balance low. Needed: {price}, Actual: {requester_balance}"
784
+ )
785
+ print(f" - Sender Address: {requester}")
786
+ sys.exit(1)
787
+
788
+ # set price 0 to not send any msg.value in request transaction for nvm type mech
789
+ price = 0
790
+
791
+ if not use_offchain:
792
+ print("Sending Mech Marketplace request...")
793
+ transaction_digest = send_marketplace_request(
794
+ crypto=crypto,
795
+ ledger_api=ledger_api,
796
+ marketplace_contract=mech_marketplace_contract,
797
+ gas_limit=mech_config.gas_limit,
798
+ price=price,
799
+ prompt=prompt,
800
+ tool=tool,
801
+ method_args_data=mech_marketplace_request_config,
802
+ extra_attributes=extra_attributes,
803
+ retries=retries,
804
+ timeout=timeout,
805
+ sleep=sleep,
806
+ )
807
+
808
+ if not transaction_digest:
809
+ print("Unable to send request")
810
+ return None
811
+
812
+ transaction_url_formatted = mech_config.transaction_url.format(
813
+ transaction_digest=transaction_digest
814
+ )
815
+ print(f" - Transaction sent: {transaction_url_formatted}")
816
+ print(" - Waiting for transaction receipt...")
817
+
818
+ request_id = watch_for_marketplace_request_id(
819
+ marketplace_contract=mech_marketplace_contract,
820
+ ledger_api=ledger_api,
821
+ tx_hash=transaction_digest,
822
+ )
823
+ request_id_int = int.from_bytes(bytes.fromhex(request_id), byteorder="big")
824
+ print(f" - Created on-chain request with ID {request_id_int}")
825
+ print("")
826
+
827
+ data_url = wait_for_marketplace_data_url(
828
+ request_id=request_id,
829
+ wss=wss,
830
+ mech_contract=mech_contract,
831
+ subgraph_url=mech_config.subgraph_url,
832
+ deliver_signature=marketplace_deliver_event_signature,
833
+ ledger_api=ledger_api,
834
+ crypto=crypto,
835
+ confirmation_type=confirmation_type,
836
+ )
837
+
838
+ if data_url:
839
+ print(f" - Data arrived: {data_url}")
840
+ data = requests.get(f"{data_url}/{request_id_int}", timeout=30).json()
841
+ print(" - Data from agent:")
842
+ print(json.dumps(data, indent=2))
843
+ return data
844
+ return None
845
+
846
+ print("Sending Offchain Mech Marketplace request...")
847
+ response = send_offchain_marketplace_request(
848
+ crypto=crypto,
849
+ marketplace_contract=mech_marketplace_contract,
850
+ prompt=prompt,
851
+ tool=tool,
852
+ method_args_data=mech_marketplace_request_config,
853
+ extra_attributes=extra_attributes,
854
+ retries=retries,
855
+ timeout=timeout,
856
+ sleep=sleep,
857
+ )
858
+
859
+ if not response:
860
+ return None
861
+
862
+ request_id = response["request_id"]
863
+ print(f" - Created off-chain request with ID {request_id}")
864
+ print("")
865
+
866
+ # @note as we are directly querying data from done task list, we get the full data instead of the ipfs hash
867
+ print("Waiting for Offchain Mech Marketplace deliver...")
868
+ data = wait_for_offchain_marketplace_data(
869
+ request_id=request_id,
870
+ )
871
+
872
+ if data:
873
+ task_result = data["task_result"]
874
+ data_url = f"https://gateway.autonolas.tech/ipfs/f01701220{task_result}"
875
+ print(f" - Data arrived: {data_url}")
876
+ data = requests.get(f"{data_url}/{request_id}", timeout=30).json()
877
+ print(" - Data from agent:")
878
+ print(json.dumps(data, indent=2))
879
+ return data
880
+ return None