mech-client 0.8.1__py3-none-any.whl → 0.9.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.
Files changed (30) hide show
  1. mech_client/__init__.py +1 -1
  2. mech_client/cli.py +8 -0
  3. mech_client/marketplace_interact.py +18 -6
  4. {mech_client-0.8.1.dist-info → mech_client-0.9.0.dist-info}/METADATA +11 -1
  5. {mech_client-0.8.1.dist-info → mech_client-0.9.0.dist-info}/RECORD +30 -8
  6. scripts/__init__.py +1 -0
  7. scripts/benchmark.sh +32 -0
  8. scripts/bump.py +316 -0
  9. scripts/deposit_native.py +118 -0
  10. scripts/deposit_token.py +187 -0
  11. scripts/nvm_subscribe.py +51 -0
  12. scripts/nvm_subscription/contracts/agreement_manager.py +44 -0
  13. scripts/nvm_subscription/contracts/base_contract.py +75 -0
  14. scripts/nvm_subscription/contracts/did_registry.py +91 -0
  15. scripts/nvm_subscription/contracts/escrow_payment.py +85 -0
  16. scripts/nvm_subscription/contracts/lock_payment.py +76 -0
  17. scripts/nvm_subscription/contracts/nft.py +47 -0
  18. scripts/nvm_subscription/contracts/nft_sales.py +119 -0
  19. scripts/nvm_subscription/contracts/subscription_provider.py +107 -0
  20. scripts/nvm_subscription/contracts/token.py +87 -0
  21. scripts/nvm_subscription/contracts/transfer_nft.py +81 -0
  22. scripts/nvm_subscription/envs/base.env +10 -0
  23. scripts/nvm_subscription/envs/gnosis.env +10 -0
  24. scripts/nvm_subscription/manager.py +265 -0
  25. scripts/nvm_subscription/resources/networks.json +56 -0
  26. scripts/utils.py +127 -0
  27. scripts/whitelist.py +5 -0
  28. {mech_client-0.8.1.dist-info → mech_client-0.9.0.dist-info}/LICENSE +0 -0
  29. {mech_client-0.8.1.dist-info → mech_client-0.9.0.dist-info}/WHEEL +0 -0
  30. {mech_client-0.8.1.dist-info → mech_client-0.9.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,265 @@
1
+ # subscription/manager.py
2
+ from enum import Enum
3
+ import json
4
+ import logging
5
+ import os
6
+ import sys
7
+ import requests
8
+
9
+ import uuid
10
+ from typing import Any, Dict, List, Literal, Union
11
+ from web3 import Web3
12
+ from eth_account import Account
13
+ from eth_typing import ChecksumAddress
14
+
15
+ from .contracts.did_registry import DIDRegistryContract
16
+ from .contracts.nft_sales import NFTSalesTemplateContract
17
+ from .contracts.lock_payment import LockPaymentConditionContract
18
+ from .contracts.transfer_nft import TransferNFTConditionContract
19
+ from .contracts.escrow_payment import EscrowPaymentConditionContract
20
+ from .contracts.agreement_manager import AgreementStorageManagerContract
21
+ from .contracts.token import SubscriptionToken
22
+ from .contracts.nft import SubscriptionNFT
23
+ from .contracts.subscription_provider import SubscriptionProvider
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28
+ config_path = os.path.join(BASE_DIR, 'resources', 'networks.json')
29
+ CONFIGS = json.load(open(config_path, 'r'))
30
+
31
+ class Network(Enum):
32
+ GNOSIS = "GNOSIS"
33
+ BASE = "BASE"
34
+
35
+
36
+ def get_variable_value(variable: str) -> str:
37
+ try:
38
+ var = os.getenv(variable)
39
+ if var is None:
40
+ raise ValueError(f"Environment variable {variable} not found")
41
+ return var
42
+ except Exception as e:
43
+ raise e
44
+
45
+
46
+ class NVMSubscriptionManager:
47
+ """
48
+ Manages the process of creating NFT-based subscription agreements
49
+ using a series of smart contracts.
50
+ """
51
+
52
+ def __init__(self, network: str, private_key: str):
53
+ """
54
+ Initialize the SubscriptionManager, including contract instances
55
+ and Web3 connection.
56
+ """
57
+ self.url = os.getenv("MECHX_CHAIN_RPC", CONFIGS[network]["nvm"]['web3ProviderUri'])
58
+ self.web3 = Web3(Web3.HTTPProvider(self.url))
59
+
60
+ self.account = Account.from_key(private_key)
61
+ self.sender: ChecksumAddress = self.web3.to_checksum_address(self.account.address)
62
+
63
+ self.did_registry = DIDRegistryContract(self.web3)
64
+ self.nft_sales = NFTSalesTemplateContract(self.web3)
65
+ self.lock_payment = LockPaymentConditionContract(self.web3)
66
+ self.transfer_nft = TransferNFTConditionContract(self.web3)
67
+ self.escrow_payment = EscrowPaymentConditionContract(self.web3)
68
+ self.subscription_nft = SubscriptionNFT(self.web3)
69
+ self.subscription_provider = SubscriptionProvider(self.web3)
70
+
71
+ # load the subscription token to be used for base
72
+ if network == 'BASE':
73
+ self.subscription_token = SubscriptionToken(self.web3)
74
+
75
+ self.agreement_storage_manager = AgreementStorageManagerContract(self.web3)
76
+
77
+ self.token_address = self.web3.to_checksum_address(get_variable_value("TOKEN_ADDRESS"))
78
+ self.subscription_nft_address = self.web3.to_checksum_address(get_variable_value("SUBSCRIPTION_NFT_ADDRESS"))
79
+ self.subscription_credits = int(os.getenv("SUBSCRIPTION_CREDITS", "1"))
80
+ self.amounts = [int(os.getenv("PLAN_FEE_NVM", "0")), int(os.getenv("PLAN_PRICE_MECHS", "0"))]
81
+ self.subscription_id = CONFIGS[network]["nvm"]["subscription_id"]
82
+
83
+ logger.info("SubscriptionManager initialized")
84
+
85
+ def _generate_agreement_id_seed(self, length: int = 64) -> str:
86
+ """Generate a random hex string prefixed with 0x."""
87
+ seed = ''
88
+ while len(seed) < length:
89
+ seed += uuid.uuid4().hex
90
+ return '0x' + seed[:length]
91
+
92
+ def create_subscription(self, did: str, wallet_pvt_key: str, chain_id: int) -> Dict[str, Any]:
93
+ """
94
+ Execute the workflow to create a subscription for the given DID.
95
+
96
+ Args:
97
+ did (str): Decentralized Identifier for the asset.
98
+
99
+ Returns:
100
+ Dict[str, Any]: A dictionary containing transaction status and receipt.
101
+ """
102
+ logger.info(f"Creating subscription for DID: {did}")
103
+
104
+ did = did.replace("did:nv:", "0x")
105
+
106
+ ddo = self.did_registry.get_ddo(did)
107
+ service = next((s for s in ddo.get("service", []) if s.get("type") == "nft-sales"), None)
108
+ if not service:
109
+ logger.error("No nft-sales service found in DDO")
110
+ return {"status": "error", "message": "No nft-sales service in DDO"}
111
+
112
+ self.publisher = service["templateId"]
113
+
114
+ conditions = service["attributes"]["serviceAgreementTemplate"]["conditions"]
115
+ reward_address = self.escrow_payment.address
116
+ receivers = conditions[0]["parameters"][-1]["value"]
117
+
118
+ agreement_id_seed = self._generate_agreement_id_seed()
119
+ agreement_id = self.agreement_storage_manager.agreement_id(agreement_id_seed, self.sender)
120
+
121
+ # Condition hashes
122
+ lock_hash = self.lock_payment.hash_values(did, reward_address, self.token_address, self.amounts, receivers)
123
+ lock_id = self.lock_payment.generate_id(agreement_id, lock_hash)
124
+
125
+ transfer_hash = self.transfer_nft.hash_values(
126
+ did,
127
+ ddo["owner"],
128
+ self.sender,
129
+ self.subscription_credits,
130
+ lock_id,
131
+ self.subscription_nft_address,
132
+ False
133
+ )
134
+ transfer_id = self.transfer_nft.generate_id(agreement_id, transfer_hash)
135
+
136
+ escrow_hash = self.escrow_payment.hash_values(
137
+ did,
138
+ self.amounts,
139
+ receivers,
140
+ self.sender,
141
+ reward_address,
142
+ self.token_address,
143
+ lock_id,
144
+ transfer_id
145
+ )
146
+ escrow_id = self.escrow_payment.generate_id(agreement_id, escrow_hash)
147
+
148
+ user_credit_balance_before = self.subscription_nft.get_balance(
149
+ self.sender, self.subscription_id
150
+ )
151
+ print(f"Sender credits Before Purchase: {user_credit_balance_before}")
152
+
153
+ # we set value as xdai is used as subscription for gnosis
154
+ value_eth = 1
155
+ if chain_id == 8453:
156
+ # for base, usdc is used and so we don't send any value
157
+ value_eth = 0
158
+
159
+ approve_tx = self.subscription_token.build_approve_token_tx(
160
+ sender=self.sender,
161
+ to=self.lock_payment.address,
162
+ amount=10**6,
163
+ chain_id=chain_id,
164
+ )
165
+ signed_tx = self.web3.eth.account.sign_transaction(approve_tx, private_key=wallet_pvt_key)
166
+ tx_hash = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
167
+ receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
168
+
169
+ if receipt["status"] == 1:
170
+ logger.info("Approve transaction validated successfully")
171
+ logger.info({"status": "success", "tx_hash": tx_hash.hex()})
172
+ else:
173
+ logger.error("Approve transaction failed")
174
+ return {"status": "failed", "receipt": dict(receipt)}
175
+
176
+ # Build transaction
177
+ tx = self.nft_sales.build_create_agreement_tx(
178
+ agreement_id_seed=agreement_id_seed,
179
+ did=did,
180
+ condition_seeds=[lock_hash, transfer_hash, escrow_hash],
181
+ timelocks=[0, 0, 0],
182
+ timeouts=[0, 90, 0],
183
+ publisher=self.sender,
184
+ service_index=0,
185
+ reward_address=reward_address,
186
+ token_address=self.token_address,
187
+ amounts=self.amounts,
188
+ receivers=receivers,
189
+ sender=self.sender,
190
+ value_eth=value_eth,
191
+ chain_id=chain_id
192
+ )
193
+
194
+ signed_tx = self.web3.eth.account.sign_transaction(tx, private_key=wallet_pvt_key)
195
+ tx_hash = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
196
+ receipt = self.web3.eth.wait_for_transaction_receipt(transaction_hash=tx_hash)
197
+
198
+ if receipt["status"] == 1:
199
+ logger.info("Subscription transaction validated successfully")
200
+ logger.info({"status": "success", "tx_hash": tx_hash.hex()})
201
+ else:
202
+ logger.error("Subscription transaction failed")
203
+ return {"status": "failed", "receipt": dict(receipt)}
204
+
205
+ fulfill_for_delegate_params = (
206
+ # nftHolder
207
+ ddo["owner"],
208
+ # nftReceiver
209
+ self.sender,
210
+ # nftAmount
211
+ self.subscription_credits,
212
+ # lockPaymentCondition
213
+ "0x" + lock_id.hex(),
214
+ # nftContractAddress
215
+ self.subscription_nft_address,
216
+ # transfer
217
+ False,
218
+ # expirationBlock
219
+ 0,
220
+ )
221
+
222
+ fulfill_params = (
223
+ # amounts
224
+ self.amounts,
225
+ # receivers
226
+ receivers,
227
+ # returnAddress
228
+ self.sender,
229
+ # lockPaymentAddress
230
+ reward_address,
231
+ # tokenAddress
232
+ self.token_address,
233
+ # lockCondition
234
+ "0x" + lock_id.hex(),
235
+ # releaseCondition
236
+ "0x" + transfer_id.hex(),
237
+ )
238
+
239
+ fulfill_tx = self.subscription_provider.build_create_fulfill_tx(
240
+ agreement_id_seed="0x" + agreement_id.hex(),
241
+ did=did,
242
+ fulfill_for_delegate_params=fulfill_for_delegate_params,
243
+ fulfill_params=fulfill_params,
244
+ sender=self.sender,
245
+ value_eth=0,
246
+ chain_id=chain_id,
247
+ )
248
+
249
+ signed_tx = self.web3.eth.account.sign_transaction(
250
+ fulfill_tx, private_key=wallet_pvt_key
251
+ )
252
+ tx_hash = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
253
+ receipt = self.web3.eth.wait_for_transaction_receipt(transaction_hash=tx_hash)
254
+
255
+ user_credit_balance_after = self.subscription_nft.get_balance(
256
+ self.sender, self.subscription_id
257
+ )
258
+ print(f"Sender credits After Purchase: {user_credit_balance_after}")
259
+
260
+ if receipt["status"] == 1:
261
+ logger.info("Subscription purchased transaction successfully")
262
+ return {"status": "success", "tx_hash": tx_hash.hex()}
263
+ else:
264
+ logger.error("Subscription purchase transaction failed")
265
+ return {"status": "failed", "receipt": dict(receipt)}
@@ -0,0 +1,56 @@
1
+ {
2
+ "BASE": {
3
+ "envDescription": "The Base production environment where users can interact with Nevermined protocol.",
4
+ "envUrl": "https://base.nevermined.app/",
5
+ "envName": "Base",
6
+ "isProduction": true,
7
+ "nvm": {
8
+ "chainId": 8453,
9
+ "appUrl": "https://base.nevermined.app",
10
+ "web3ProviderUri": "https://1rpc.io/base",
11
+ "marketplaceUri": "https://marketplace-api.base.nevermined.app",
12
+ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public",
13
+ "neverminedNodeUri": "https://node.base.nevermined.app",
14
+ "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1",
15
+ "neverminedBackendUri": "https://one-backend.base.nevermined.app",
16
+ "subscription_id": "50413016362950485991271150743187499870906117744058208429036668344255336389082",
17
+ "verbose": true
18
+ },
19
+ "nativeToken": "ETH",
20
+ "networkName": "base",
21
+ "contractsVersion": "3.5.7",
22
+ "tagName": "public",
23
+ "etherscanUrl": "https://basescan.org",
24
+ "erc20TokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
25
+ "gasMultiplier": 0,
26
+ "gasPriceMultiplier": 0,
27
+ "externalNetwork": true
28
+ },
29
+ "GNOSIS": {
30
+ "envDescription": "The Gnosis production environment where users can interact with Nevermined protocol.",
31
+ "envUrl": "https://gnosis.nevermined.app/",
32
+ "envName": "Gnosis",
33
+ "isProduction": true,
34
+ "nvm": {
35
+ "chainId": 100,
36
+ "appUrl": "https://gnosis.nevermined.app",
37
+ "web3ProviderUri": "https://rpc.gnosischain.com/",
38
+ "marketplaceUri": "https://marketplace-api.gnosis.nevermined.app",
39
+ "graphHttpUri": "https://api.thegraph.com/subgraphs/name/nevermined-io/public",
40
+ "neverminedNodeUri": "https://node.gnosis.nevermined.app",
41
+ "neverminedNodeAddress": "0x824dbcE5E9C96C5b8ce2A35a25a5ab87eD1D00b1",
42
+ "neverminedBackendUri": "https://one-backend.gnosis.nevermined.app",
43
+ "subscription_id": "79922471236808376650579296325311587488574799199590951702510416090224804407991",
44
+ "verbose": true
45
+ },
46
+ "nativeToken": "xDAI",
47
+ "networkName": "gnosis",
48
+ "contractsVersion": "3.5.7",
49
+ "tagName": "public",
50
+ "etherscanUrl": "https://gnosisscan.io",
51
+ "erc20TokenAddress": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83",
52
+ "gasMultiplier": 1.1,
53
+ "gasPriceMultiplier": 0,
54
+ "externalNetwork": true
55
+ }
56
+ }
scripts/utils.py ADDED
@@ -0,0 +1,127 @@
1
+ import json
2
+ from pathlib import Path
3
+ from aea_ledger_ethereum import EthereumApi
4
+ from web3.contract import Contract as Web3Contract
5
+ from mech_client.marketplace_interact import get_contract, CHAIN_TO_PRICE_TOKEN
6
+
7
+
8
+ # based on mech configs
9
+ VALID_CHAINS = [
10
+ "gnosis",
11
+ "arbitrum",
12
+ "polygon",
13
+ "base",
14
+ "celo",
15
+ "optimism",
16
+ ]
17
+
18
+ CHAIN_TO_NATIVE_BALANCE_TRACKER = {
19
+ 100: "0x21cE6799A22A3Da84B7c44a814a9c79ab1d2A50D",
20
+ 42161: "",
21
+ 137: "",
22
+ 8453: "0xB3921F8D8215603f0Bd521341Ac45eA8f2d274c1",
23
+ 42220: "",
24
+ 10: "",
25
+ }
26
+
27
+ CHAIN_TO_TOKEN_BALANCE_TRACKER = {
28
+ 100: "0x53Bd432516707a5212A70216284a99A563aAC1D1",
29
+ 42161: "",
30
+ 137: "",
31
+ 8453: "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237",
32
+ 42220: "",
33
+ 10: "",
34
+ }
35
+
36
+
37
+ def print_box(text: str, margin: int = 1, character: str = "=") -> None:
38
+ """Print text centered within a box."""
39
+
40
+ lines = text.split("\n")
41
+ text_length = max(len(line) for line in lines)
42
+ length = text_length + 2 * margin
43
+
44
+ border = character * length
45
+ margin_str = " " * margin
46
+
47
+ print(border)
48
+ print(f"{margin_str}{text}{margin_str}")
49
+ print(border)
50
+ print()
51
+
52
+
53
+ def print_title(text: str) -> None:
54
+ """Print title."""
55
+ print()
56
+ print_box(text, 4, "=")
57
+
58
+
59
+ def input_with_default_value(prompt: str, default_value: str) -> str:
60
+ user_input = input(f"{prompt} [{default_value}]: ")
61
+ return str(user_input) if user_input else default_value
62
+
63
+
64
+ def input_select_chain() -> str:
65
+ """Chose a single option from the offered ones"""
66
+ user_input = input(f"Chose one of the following options {VALID_CHAINS}: ").lower()
67
+ if user_input in VALID_CHAINS:
68
+ return user_input
69
+ else:
70
+ print("Invalid option selected. Please try again.")
71
+ return input_select_chain()
72
+
73
+
74
+ def get_native_balance_tracker_contract(
75
+ ledger_api: EthereumApi, chain_id: int
76
+ ) -> Web3Contract:
77
+ with open(
78
+ Path(__file__).parent.parent
79
+ / "mech_client"
80
+ / "abis"
81
+ / "BalanceTrackerFixedPriceNative.json",
82
+ encoding="utf-8",
83
+ ) as f:
84
+ abi = json.load(f)
85
+
86
+ native_balance_tracker_contract = get_contract(
87
+ contract_address=CHAIN_TO_NATIVE_BALANCE_TRACKER[chain_id],
88
+ abi=abi,
89
+ ledger_api=ledger_api,
90
+ )
91
+ return native_balance_tracker_contract
92
+
93
+
94
+ def get_token_balance_tracker_contract(
95
+ ledger_api: EthereumApi, chain_id: int
96
+ ) -> Web3Contract:
97
+ with open(
98
+ Path(__file__).parent.parent
99
+ / "mech_client"
100
+ / "abis"
101
+ / "BalanceTrackerFixedPriceToken.json",
102
+ encoding="utf-8",
103
+ ) as f:
104
+ abi = json.load(f)
105
+
106
+ token_balance_tracker_contract = get_contract(
107
+ contract_address=CHAIN_TO_TOKEN_BALANCE_TRACKER[chain_id],
108
+ abi=abi,
109
+ ledger_api=ledger_api,
110
+ )
111
+ return token_balance_tracker_contract
112
+
113
+
114
+ def get_token_contract(ledger_api: EthereumApi, chain_id: int) -> Web3Contract:
115
+ with open(
116
+ Path(__file__).parent.parent / "mech_client" / "abis" / "IToken.json",
117
+ encoding="utf-8",
118
+ ) as f:
119
+ abi = json.load(f)
120
+
121
+ token_contract = get_contract(
122
+ contract_address=CHAIN_TO_PRICE_TOKEN[chain_id],
123
+ abi=abi,
124
+ ledger_api=ledger_api,
125
+ )
126
+
127
+ return token_contract
scripts/whitelist.py ADDED
@@ -0,0 +1,5 @@
1
+ directory # unused attribute (mech_client/acn.py:85)
2
+ directory # unused attribute (mech_client/acn.py:100)
3
+ directory # unused attribute (mech_client/acn.py:116)
4
+ payment_data # unused variable (mech_client/interact.py:111)
5
+ AGENT_QUERY_TEMPLATE # unused variable (mech_client/subgraph.py:30)