mech-client 0.8.0__py3-none-any.whl → 0.8.2__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,119 @@
1
+ # subscription/contracts/nft_sales.py
2
+ import logging
3
+ from typing import List, Any, Dict, Union
4
+ from web3 import Web3
5
+ from eth_typing import ChecksumAddress
6
+ from web3.types import ENS
7
+
8
+ from .base_contract import BaseContract
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class NFTSalesTemplateContract(BaseContract):
14
+ """
15
+ Wrapper class for the NFTSalesTemplate smart contract. Provides a method
16
+ to build a transaction for creating an agreement and paying escrow.
17
+ """
18
+
19
+ def __init__(self, w3: Web3):
20
+ """
21
+ Initialize the NFTSalesTemplateContract.
22
+
23
+ Args:
24
+ w3 (Web3): An instance of Web3 connected to an Ethereum network.
25
+ """
26
+ logger.debug("Initializing NFTSalesTemplateContract")
27
+ super().__init__(w3, name="NFTSalesTemplate")
28
+ logger.info("NFTSalesTemplateContract initialized")
29
+
30
+ def build_create_agreement_tx(
31
+ self,
32
+ agreement_id_seed: str,
33
+ did: str,
34
+ condition_seeds: List[bytes],
35
+ timelocks: List[int],
36
+ timeouts: List[int],
37
+ publisher: str,
38
+ service_index: int,
39
+ reward_address: str,
40
+ token_address: str,
41
+ amounts: List[int],
42
+ receivers: List[str],
43
+ sender: str,
44
+ value_eth: float,
45
+ gas: int = 600_000,
46
+ chain_id: int = 100
47
+ ) -> Dict[str, Any]:
48
+ """
49
+ Build a transaction dictionary to create an agreement and pay escrow.
50
+
51
+ Args:
52
+ agreement_id_seed (str): Unique identifier seed for the agreement.
53
+ did (str): Decentralized identifier.
54
+ condition_seeds (List[bytes]): List of hashed condition values.
55
+ timelocks (List[int]): Time locks for each condition.
56
+ timeouts (List[int]): Timeouts for each condition.
57
+ publisher (str): Ethereum address of the publisher.
58
+ service_index (int): Index of the service in the agreement.
59
+ reward_address (str): Address to receive the reward.
60
+ token_address (str): ERC20 token address.
61
+ amounts (List[int]): Payment amounts.
62
+ receivers (List[str]): List of payment receiver addresses.
63
+ sender (str): Ethereum address initiating the transaction.
64
+ value_eth (float): ETH value to include in the transaction.
65
+ gas (int, optional): Gas limit. Defaults to 600,000.
66
+ chain_id (int, optional): Ethereum network chain ID. Defaults to 100.
67
+
68
+ Returns:
69
+ Dict[str, Any]: Unsigned transaction dictionary.
70
+ """
71
+ logger.debug("Building transaction for createAgreementAndPayEscrow")
72
+ logger.debug(f"agreement_id_seed: {agreement_id_seed}")
73
+ logger.debug(f"did: {did}")
74
+ logger.debug(f"condition_seeds: {condition_seeds}")
75
+ logger.debug(f"timelocks: {timelocks}, timeouts: {timeouts}")
76
+ logger.debug(f"publisher: {publisher}, service_index: {service_index}")
77
+ logger.debug(f"reward_address: {reward_address}, token_address: {token_address}")
78
+ logger.debug(f"amounts: {amounts}, receivers: {receivers}")
79
+ logger.debug(f"sender: {sender}, value_eth: {value_eth}")
80
+
81
+ # Convert sender to a checksum address to ensure type safety
82
+ sender_address: ChecksumAddress = self.w3.to_checksum_address(sender)
83
+ nonce = self.w3.eth.get_transaction_count(sender_address)
84
+ logger.debug(f"Nonce for sender {sender_address}: {nonce}")
85
+
86
+ latest_block = self.w3.eth.get_block("latest")
87
+ base_fee = latest_block["baseFeePerGas"]
88
+ max_priority_fee = self.w3.eth.max_priority_fee
89
+
90
+ # Build the transaction using the contract method
91
+ tx = self.functions().createAgreementAndPayEscrow(
92
+ agreement_id_seed,
93
+ did,
94
+ condition_seeds,
95
+ timelocks,
96
+ timeouts,
97
+ self.w3.to_checksum_address(publisher),
98
+ service_index,
99
+ self.w3.to_checksum_address(reward_address),
100
+ self.w3.to_checksum_address(token_address),
101
+ amounts,
102
+ [self.w3.to_checksum_address(r) for r in receivers]
103
+ ).build_transaction({
104
+ "from": sender_address,
105
+ "value": self.w3.to_wei(value_eth, "ether"),
106
+ "chainId": chain_id,
107
+ "gas": gas,
108
+ "nonce": nonce,
109
+ })
110
+ gas = self.w3.eth.estimate_gas(tx)
111
+ tx.update({
112
+ "gas": gas,
113
+ "maxFeePerGas": base_fee + max_priority_fee,
114
+ "maxPriorityFeePerGas": max_priority_fee,
115
+ })
116
+
117
+ logger.info(f"Transaction built successfully for agreement ID: {agreement_id_seed}")
118
+ logger.debug(f"Transaction details: {tx}")
119
+ return tx
@@ -0,0 +1,107 @@
1
+ # subscription/contracts/subscription_provider.py
2
+ import logging
3
+ from typing import Union, List, Dict, Any
4
+ from web3 import Web3
5
+ from eth_typing import ChecksumAddress
6
+ from web3.types import ENS
7
+
8
+ from .base_contract import BaseContract
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SubscriptionProvider(BaseContract):
14
+ """
15
+ Wrapper for the Token smart contract. Supports token balance
16
+ """
17
+
18
+ def __init__(self, w3: Web3):
19
+ """
20
+ Initialize the Subscription provider instance.
21
+
22
+ Args:
23
+ w3 (Web3): A connected Web3 instance.
24
+ """
25
+ logger.debug("Initializing Subscription provider")
26
+ super().__init__(w3, name="SubscriptionProvider")
27
+ logger.info("Subscription provider initialized")
28
+
29
+ def build_create_fulfill_tx(
30
+ self,
31
+ agreement_id_seed: str,
32
+ did: str,
33
+ fulfill_for_delegate_params: tuple,
34
+ fulfill_params: tuple,
35
+ sender: str,
36
+ value_eth: float,
37
+ gas: int = 450_000,
38
+ chain_id: int = 100,
39
+ ) -> Dict[str, Any]:
40
+ """
41
+ Build a transaction dictionary to create a fulfill tx.
42
+
43
+ Args:
44
+ agreement_id_seed (str): Unique identifier seed for the agreement.
45
+ did (str): Decentralized identifier.
46
+ condition_seeds (List[bytes]): List of hashed condition values.
47
+ timelocks (List[int]): Time locks for each condition.
48
+ timeouts (List[int]): Timeouts for each condition.
49
+ publisher (str): Ethereum address of the publisher.
50
+ service_index (int): Index of the service in the agreement.
51
+ reward_address (str): Address to receive the reward.
52
+ token_address (str): ERC20 token address.
53
+ amounts (List[int]): Payment amounts.
54
+ receivers (List[str]): List of payment receiver addresses.
55
+ sender (str): Ethereum address initiating the transaction.
56
+ value_eth (float): ETH value to include in the transaction.
57
+ gas (int, optional): Gas limit. Defaults to 450,000.
58
+ chain_id (int, optional): Ethereum network chain ID. Defaults to 100.
59
+
60
+ Returns:
61
+ Dict[str, Any]: Unsigned transaction dictionary.
62
+ """
63
+ logger.debug("Building transaction for fulfill")
64
+ logger.debug(f"agreement_id_seed: {agreement_id_seed}")
65
+ logger.debug(f"did: {did}")
66
+ logger.debug(f"sender: {sender}, value_eth: {value_eth}")
67
+
68
+ # Convert sender to a checksum address to ensure type safety
69
+ sender_address: ChecksumAddress = self.w3.to_checksum_address(sender)
70
+ nonce = self.w3.eth.get_transaction_count(sender_address)
71
+ logger.debug(f"Nonce for sender {sender_address}: {nonce}")
72
+
73
+ latest_block = self.w3.eth.get_block("latest")
74
+ base_fee = latest_block["baseFeePerGas"]
75
+ max_priority_fee = self.w3.eth.max_priority_fee
76
+
77
+ logger.debug(f"fulfill_for_delegate_params: {fulfill_for_delegate_params}")
78
+ logger.debug(f"fulfill_params: {fulfill_params}")
79
+
80
+ # Build the transaction using the contract method
81
+ tx = (
82
+ self.functions()
83
+ .fulfill(
84
+ agreement_id_seed, did, fulfill_for_delegate_params, fulfill_params
85
+ )
86
+ .build_transaction(
87
+ {
88
+ "from": sender_address,
89
+ "value": self.w3.to_wei(value_eth, "ether"),
90
+ "chainId": chain_id,
91
+ "gas": gas,
92
+ "nonce": nonce,
93
+ }
94
+ )
95
+ )
96
+ gas = self.w3.eth.estimate_gas(tx)
97
+ tx.update(
98
+ {
99
+ "gas": gas,
100
+ "maxFeePerGas": base_fee + max_priority_fee,
101
+ "maxPriorityFeePerGas": max_priority_fee,
102
+ }
103
+ )
104
+
105
+ logger.info(f"Transaction built successfully for fulfill: {agreement_id_seed}")
106
+ logger.debug(f"Transaction details: {tx}")
107
+ return tx
@@ -0,0 +1,87 @@
1
+ # subscription/contracts/transfer_nft.py
2
+ import logging
3
+ from typing import Union
4
+ from web3 import Web3
5
+ from eth_typing import ChecksumAddress
6
+ from web3.types import ENS
7
+
8
+ from .base_contract import BaseContract
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SubscriptionToken(BaseContract):
14
+ """
15
+ Wrapper for the Token smart contract. Supports approve token
16
+ """
17
+
18
+ def __init__(self, w3: Web3):
19
+ """
20
+ Initialize the Token instance.
21
+
22
+ Args:
23
+ w3 (Web3): A connected Web3 instance.
24
+ """
25
+ logger.debug("Initializing Subscription Token")
26
+ super().__init__(w3, name="SubscriptionToken")
27
+ logger.info("Token initialized")
28
+
29
+ def build_approve_token_tx(
30
+ self,
31
+ sender: Union[ChecksumAddress, ENS],
32
+ to: Union[ChecksumAddress, ENS],
33
+ amount: int,
34
+ gas: int = 60_000,
35
+ chain_id: int = 100,
36
+ ) -> bytes:
37
+ """
38
+ Compute the hash of parameters for the transfer condition.
39
+
40
+ Args:
41
+ sender (ChecksumAddress | ENS): Address sending the approve tx.
42
+ to_address (ChecksumAddress | ENS): Address getting the approval.
43
+ amount (int): Number of tokens to approve.
44
+ gas (int, optional): Gas limit. Defaults to 60,000.
45
+ chain_id (int, optional): Ethereum network chain ID. Defaults to 100.
46
+
47
+ Returns:
48
+ Dict[str, Any]: Unsigned transaction dictionary.
49
+ """
50
+ logger.debug("Approving token...")
51
+ sender_address: ChecksumAddress = self.w3.to_checksum_address(sender)
52
+ to_address: ChecksumAddress = self.w3.to_checksum_address(to)
53
+ nonce = self.w3.eth.get_transaction_count(sender_address)
54
+ logger.debug(f"Nonce for sender {sender_address}: {nonce}")
55
+
56
+ latest_block = self.w3.eth.get_block("latest")
57
+ base_fee = latest_block["baseFeePerGas"]
58
+ max_priority_fee = self.w3.eth.max_priority_fee
59
+
60
+ tx = (
61
+ self.functions()
62
+ .approve(
63
+ to_address,
64
+ amount,
65
+ )
66
+ .build_transaction(
67
+ {
68
+ "from": sender,
69
+ "value": 0,
70
+ "chainId": chain_id,
71
+ "gas": gas,
72
+ "nonce": nonce,
73
+ }
74
+ )
75
+ )
76
+ gas = self.w3.eth.estimate_gas(tx)
77
+ tx.update(
78
+ {
79
+ "gas": gas,
80
+ "maxFeePerGas": base_fee + max_priority_fee,
81
+ "maxPriorityFeePerGas": max_priority_fee,
82
+ }
83
+ )
84
+
85
+ logger.info("Transaction built successfully for token approve")
86
+ logger.debug(f"Transaction details: {tx}")
87
+ return tx
@@ -0,0 +1,81 @@
1
+ # subscription/contracts/transfer_nft.py
2
+ import logging
3
+ from typing import Union
4
+ from web3 import Web3
5
+ from eth_typing import ChecksumAddress
6
+ from web3.types import ENS
7
+
8
+ from .base_contract import BaseContract
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class TransferNFTConditionContract(BaseContract):
14
+ """
15
+ Wrapper for the TransferNFTCondition smart contract. Supports hash and ID generation.
16
+ """
17
+
18
+ def __init__(self, w3: Web3):
19
+ """
20
+ Initialize the TransferNFTConditionContract instance.
21
+
22
+ Args:
23
+ w3 (Web3): A connected Web3 instance.
24
+ """
25
+ logger.debug("Initializing TransferNFTConditionContract")
26
+ super().__init__(w3, name="TransferNFTCondition")
27
+ logger.info("TransferNFTConditionContract initialized")
28
+
29
+ def hash_values(
30
+ self,
31
+ did: str,
32
+ from_address: Union[ChecksumAddress, ENS],
33
+ to_address: Union[ChecksumAddress, ENS],
34
+ amount: int,
35
+ lock_condition_id: bytes,
36
+ nft_contract_address: Union[ChecksumAddress, ENS],
37
+ _is_transfer: bool,
38
+ ) -> bytes:
39
+ """
40
+ Compute the hash of parameters for the transfer condition.
41
+
42
+ Args:
43
+ did (str): Decentralized identifier.
44
+ from_address (ChecksumAddress | ENS): Address sending the NFT.
45
+ to_address (ChecksumAddress | ENS): Address receiving the NFT.
46
+ amount (int): Number of tokens to transfer.
47
+ lock_condition_id (bytes): Lock payment condition ID.
48
+ nft_contract_address (ChecksumAddress | ENS): NFT contract address.
49
+ is_escrow (bool): Indicates if escrow is involved.
50
+
51
+ Returns:
52
+ bytes: Hashed value.
53
+ """
54
+ logger.debug("Computing transfer NFT hash value")
55
+ hash_ = self.functions().hashValues(
56
+ did,
57
+ from_address,
58
+ to_address,
59
+ amount,
60
+ lock_condition_id,
61
+ nft_contract_address,
62
+ _is_transfer
63
+ ).call()
64
+ logger.debug(f"Transfer NFT hash: {hash_.hex()}")
65
+ return hash_
66
+
67
+ def generate_id(self, agreement_id: bytes, hash_value: bytes) -> bytes:
68
+ """
69
+ Generate the condition ID for the transfer NFT condition.
70
+
71
+ Args:
72
+ agreement_id (str): ID of the agreement.
73
+ hash_value (bytes): Hashed condition parameters.
74
+
75
+ Returns:
76
+ bytes: Condition ID.
77
+ """
78
+ logger.debug("Generating transfer NFT condition ID")
79
+ condition_id = self.functions().generateId(agreement_id, hash_value).call()
80
+ logger.info(f"Transfer NFT condition ID: {condition_id.hex()}")
81
+ return condition_id
@@ -0,0 +1,10 @@
1
+ export NETWORK_NAME="BASE"
2
+ export CHAIN_ID=8453
3
+ export SUBSCRIPTION_NFT_ADDRESS="0xd5318d1a17819f65771b6c9277534c08dd765498"
4
+ export OLAS_MARKETPLACE_ADDRESS="0xaaFBeef195BDAb1Bb6f3Dc9cEbA875Cd72499230"
5
+ export RECEIVER_PLAN="0xaaFBeef195BDAb1Bb6f3Dc9cEbA875Cd72499230"
6
+ export TOKEN_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" #USDC
7
+ export PLAN_DID="did:nv:6f74c18fae7e5c3589b99d7cd0ba317593f00dee53c81a2ba4ac2244232f99da"
8
+ export SUBSCRIPTION_CREDITS="1000000"
9
+ export PLAN_FEE_NVM="10000"
10
+ export PLAN_PRICE_MECHS="990000"
@@ -0,0 +1,10 @@
1
+ export NETWORK_NAME="GNOSIS"
2
+ export CHAIN_ID=100
3
+ export SUBSCRIPTION_NFT_ADDRESS="0x1b5DeaD7309b56ca7663b3301A503e077Be18cba"
4
+ export OLAS_MARKETPLACE_ADDRESS="0x7D686bD1fD3CFF6E45a40165154D61043af7D67c"
5
+ export RECEIVER_PLAN="0x7D686bD1fD3CFF6E45a40165154D61043af7D67c"
6
+ export TOKEN_ADDRESS="0x0000000000000000000000000000000000000000" # xDAI
7
+ export PLAN_DID="did:nv:b0b28402e5a7229804579d4ac55b98a1dd94660d7a7eb4add78e5ca856f2aab7"
8
+ export SUBSCRIPTION_CREDITS="1000000"
9
+ export PLAN_FEE_NVM="10000000000000000"
10
+ export PLAN_PRICE_MECHS="990000000000000000"
@@ -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)}