Skip to content

Commit b2cfc5c

Browse files
update app scripts
1 parent 0e36337 commit b2cfc5c

4 files changed

Lines changed: 122 additions & 63 deletions

File tree

README.MD

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ forge script script/MockSetup.s.sol --fork-url http://127.0.0.1:8545 --broadcast
366366

367367
```shell
368368
cd app
369-
eval $(echo export $(grep MNEMONIC .env))
370-
eval $(echo export $(grep CONTRACT_ADDRESS .env))
369+
eval $(echo export $(grep PRODUCT_CONTRACT_ADDRESS .env))
370+
eval $(echo export $(grep OPERATOR_WALLET_MNEMONIC .env))
371371
uv run python
372372
```
373373

@@ -377,6 +377,7 @@ Follow the steps in the python shell below.
377377
import os
378378

379379
from web3 import Web3
380+
from web3utils.chain import Chain
380381
from web3utils.contract import Contract
381382
from web3utils.wallet import Wallet
382383

@@ -385,8 +386,8 @@ w3_uri = "http://127.0.0.1:8545"
385386
w3 = Web3(Web3.HTTPProvider(w3_uri))
386387

387388
# create account using the env variable mnemonic
388-
w = Wallet.from_mnemonic(os.getenv('ETH_MNEMONIC'))
389-
operator = Wallet.from_mnemonic(os.getenv('OPERATOR_WALLET_MNEMONIC'))
389+
w = Wallet.from_mnemonic(os.getenv('ETH_MNEMONIC'), w3=w3)
390+
operator = Wallet.from_mnemonic(os.getenv('OPERATOR_WALLET_MNEMONIC'), w3=w3o)
390391

391392
# send 1 eth to operator
392393
w.transfer(w3, operator.address, 1 * 10**18)

app/web3utils/chain.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
from web3 import Web3
3+
4+
class Chain:
5+
"""Simple class to query network."""
6+
7+
w3:Web3|None = None
8+
9+
def __init__(self, w3:Web3) -> None:
10+
"""Create a new chain query object.
11+
"""
12+
self.w3 = w3
13+
14+
def w3(self) -> Web3:
15+
"""Get the web3 instance."""
16+
if not self.w3:
17+
raise ValueError("Web3 instance not provided")
18+
return self.w3
19+
20+
def id(self) -> int:
21+
"""Get the network ID."""
22+
chain_id = self.w3.net.version
23+
try:
24+
return int(chain_id)
25+
except Exception as e:
26+
raise ValueError(f"Chain Id {chain_id} is not an integer. Error: {e}")
27+
28+
def latest_block(self) -> int:
29+
"""Get the number of the latest block."""
30+
self.w3.eth.block_number
31+
32+
def block(self, block_id='latest') -> dict:
33+
"""Get a block by its ID."""
34+
return dict(self.w3.eth.get_block(block_id))
35+
36+
def timestamp(self, block_number='latest') -> int:
37+
"""Get the timestamp of a block."""
38+
return self.w3.eth.get_block(block_number)['timestamp']

app/web3utils/contract.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import json
2+
import logging
3+
4+
from functools import wraps
25
from typing import Any, Dict
3-
from loguru import logger
46

57
from web3 import Web3
68
from web3.contract import Contract as Web3Contract
79
from web3.exceptions import TimeExhausted
810
from web3.types import FilterParams
9-
from util.logging import get_logger
10-
from web3utils.wallet import Wallet
1111

12-
logger = get_logger()
12+
from web3utils.wallet import Wallet
1313

1414
class Contract:
1515

16-
FOUNDRY_OUT = "./abi"
16+
FOUNDRY_OUT = "../out"
1717
SOLIDITY_EXT = "sol"
1818
GAS = 1000000
1919
TX_TIMEOUT_SECONDS = 120
@@ -51,10 +51,10 @@ def _setup_functions(self) -> None:
5151
mutability = func.get('stateMutability')
5252

5353
if mutability in ['nonpayable', 'payable']:
54-
logger.info(f"creating {func_name}(...) tx")
54+
logging.info(f"creating {func_name}(...) tx")
5555
setattr(self, func_name, self._create_write_method(func_name))
5656
elif mutability in ['view', 'pure']:
57-
logger.info(f"creating {func_name}(...) call")
57+
logging.info(f"creating {func_name}(...) call")
5858
setattr(self, func_name, self._create_read_method(func_name))
5959

6060
def _create_read_method(self, func_name: str):
@@ -63,14 +63,26 @@ def read_method(*args, **kwargs) -> Any:
6363
modified_args = [arg.address if isinstance(arg, Wallet) else arg for arg in args]
6464
return getattr(self.contract.functions, func_name)(*modified_args, **kwargs).call()
6565
except Exception as e:
66-
logger.warning(f"Error calling function '{func_name}': {e}")
66+
logging.warning(f"Error calling function '{func_name}': {e}")
6767
return None
6868

69-
# Optionally, add docstrings or additional attributes here
70-
read_method.__name__ = func_name
71-
read_method.__doc__ = f"Calls the '{func_name}' function of the contract."
69+
# add docstrings signature and selector
70+
self._amend_method(read_method, func_name)
71+
7272
return read_method
7373

74+
def _amend_method(self, method, name):
75+
method.__name__ = name
76+
method.__doc__ = f"Calls the '{name}' contract function."
77+
78+
signature = getattr(self.contract.functions, name).signature
79+
method.signature = signature
80+
method.argument_names = getattr(self.contract.functions, name).argument_names
81+
method.inputs = getattr(self.contract.functions, name).abi['inputs']
82+
method.outputs = getattr(self.contract.functions, name).abi['outputs']
83+
method.selector = Web3.keccak(text=signature)[:4]
84+
method.selector_hex = method.selector.hex()
85+
7486
def _get_tx_params(self, args:tuple) -> Dict[str, Any]:
7587
if len(args) == 0:
7688
raise ValueError("No transaction parameters provided.")
@@ -95,38 +107,31 @@ def write_method(*args) -> str:
95107

96108
try:
97109
wallet = tx_params['from']
98-
logger.info(f"Sending transaction for function '{func_name}' with wallet: {wallet.address}")
99110

100111
# create tx properties
101112
chain_id = self.w3.eth.chain_id
102113
gas = tx_params.get('gas', self.GAS)
103114
gas_price = tx_params.get('gasPrice', self.w3.eth.gas_price)
104-
gas_limit = tx_params.get('gasLimit', None)
105115
nonce = self.w3.eth.get_transaction_count(wallet.address)
106116

107117
# transform wallet args to addresses (str)
108118
modified_args = [arg.address if isinstance(arg, Wallet) else arg for arg in function_args]
109119

110-
options = {
120+
# create tx
121+
txn = getattr(self.contract.functions, func_name)(*modified_args).build_transaction({
111122
'chainId': chain_id,
112123
'gas': gas,
113124
'gasPrice': gas_price,
114125
'nonce': nonce,
115-
}
116-
117-
if gas_limit:
118-
options['gas'] = gas_limit
119-
120-
# create tx
121-
txn = getattr(self.contract.functions, func_name)(*modified_args).build_transaction(options)
126+
})
122127

123128
# sign tx
124129
private_key = bytes(wallet.account.key)
125130
signed_txn = self.w3.eth.account.sign_transaction(txn, private_key=private_key)
126131

127132
# send signed transaction
128133
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
129-
logger.info(f"Transaction sent: {tx_hash.hex()}")
134+
logging.info(f"Transaction sent: {tx_hash.hex()}")
130135

131136
if 'timeout' not in tx_params:
132137
timeout = self.TX_TIMEOUT_SECONDS
@@ -136,25 +141,23 @@ def write_method(*args) -> str:
136141
try:
137142
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
138143
except TimeExhausted:
139-
logger.warning(f"Transaction timeout after {timeout} seconds.")
144+
logging.warning(f"Transaction timeout after {timeout} seconds.")
140145
return tx_hash.hex()
141146

142147
if receipt['status'] == 1:
143-
logger.info(f"Transaction successful: {receipt}")
148+
logging.info(f"Transaction successful: {receipt}")
144149
else:
145-
logger.warning(f"Transaction failed: {receipt}")
150+
logging.warning(f"Transaction failed: {receipt}")
146151

147152
return tx_hash.hex()
148153

149154
except Exception as e:
150-
logger.warning(f"Error sending transaction for function '{func_name}': {e}")
151-
if 'tx_hash' in locals():
152-
logger.warning(f"Transaction hash: {tx_hash.hex()}")
153-
return tx_hash.hex()
154-
return None
155+
logging.warning(f"Error sending transaction for function '{func_name}': {e}")
156+
return tx_hash.hex()
157+
158+
# add docstrings signature and selector
159+
self._amend_method(write_method, func_name)
155160

156-
write_method.__name__ = func_name
157-
write_method.__doc__ = f"Sends a transaction to the '{func_name}' function of the contract."
158161
return write_method
159162

160163
def _load_abi(self, contract:str, out_path:str) -> Dict[str, Any]:

app/web3utils/wallet.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
from typing import Any
1+
from typing import Any, Union
22

33
from eth_account import Account
44
from eth_account.hdaccount import ETHEREUM_DEFAULT_PATH
55
from eth_account.signers.local import LocalAccount
66
from eth_account.types import Language
77

8-
from util.password import generate_password
98
from web3 import Web3
109

10+
from util.password import generate_password
11+
1112

1213
class Wallet:
1314
"""Simple class for wallet creation."""
@@ -27,6 +28,7 @@ class Wallet:
2728
language: Language | None
2829
path: str | None
2930
index: int | None
31+
w3: Web3 | None
3032

3133
def __init__(self) -> None:
3234
"""Create a new wallet.
@@ -44,41 +46,52 @@ def __init__(self) -> None:
4446
self.language = None
4547
self.path = ETHEREUM_DEFAULT_PATH
4648
self.index = Wallet.INDEX_DEFAULT
49+
self.w3 = None
50+
51+
52+
def nonce(self) -> int:
53+
if not self.w3:
54+
raise ValueError("Web3 instance not provided")
55+
56+
return self.w3.eth.get_transaction_count(self.address)
4757

48-
def transfer(
49-
self,
50-
w3:Web3,
51-
to:str,
52-
amount:int,
53-
gas:int|None=None,
54-
gas_price:int|None=None,
55-
) -> str:
56-
"""Transfer a specified amount of wei to a specified address."""
57-
if not self.account:
58-
raise ValueError("Account not initialized")
59-
60-
if not gas:
61-
gas = w3.eth.estimate_gas({
62-
'to': to,
63-
'from': self.address,
64-
'value': amount
65-
})
66-
67-
if not gas_price:
68-
gas_price = w3.eth.gas_price
58+
59+
def balance(self) -> int:
60+
if not self.w3:
61+
raise ValueError("Web3 instance not provided")
62+
63+
return self.w3.eth.get_balance(self.address)
64+
65+
66+
def transfer(self, to: Union [str, "Wallet"], amount: int, gas_price: int = None) -> str:
67+
if not self.w3:
68+
raise ValueError("Web3 instance not provided")
69+
70+
if isinstance(to, Wallet):
71+
to = to.address
72+
73+
nonce = self.nonce()
74+
chain_id = self.w3.eth.chain_id
75+
gas = 21000
76+
gas_price = gas_price or self.w3.eth.gas_price
6977

7078
tx = {
7179
"to": to,
7280
"value": amount,
73-
"nonce": w3.eth.get_transaction_count(self.address),
81+
"nonce": nonce,
82+
"chainId": chain_id, # only replay-protected (EIP-155) transactions allowed over RPC
7483
"gas": gas,
7584
"gasPrice": gas_price,
7685
}
7786

78-
signed_tx = self.account.sign_transaction(tx)
79-
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
87+
try:
88+
signed = self.account.sign_transaction(tx)
89+
tx_hash = self.w3.eth.send_raw_transaction(signed.raw_transaction)
90+
91+
except Exception as e:
92+
raise ValueError(f"Error sending transaction: {e}")
8093

81-
return tx_hash.hex()
94+
return tx_hash.to_0x_hex()
8295

8396

8497
@classmethod
@@ -88,6 +101,7 @@ def create(
88101
language: Language = LANGUAGE_DEFAULT,
89102
index: int = INDEX_DEFAULT,
90103
password: str | None = None,
104+
w3: Web3 | None = None,
91105
print_address: bool = True, # noqa: FBT001, FBT002
92106
) -> "Wallet":
93107
"""Create a new wallet."""
@@ -116,6 +130,7 @@ def create(
116130

117131
wallet.password = password
118132
wallet.vault = wallet.account.encrypt(password) # type: ignore # noqa: PGH003
133+
wallet.w3 = w3
119134

120135
return wallet
121136

@@ -125,6 +140,7 @@ def from_mnemonic(
125140
index: int = INDEX_DEFAULT,
126141
password: str = generate_password(),
127142
path: str = ETHEREUM_DEFAULT_PATH,
143+
w3: Web3 | None = None,
128144
) -> "Wallet":
129145
"""Create a new wallet from a provided mnemonic."""
130146
Wallet.validate_mnemonic(mnemonic)
@@ -134,6 +150,7 @@ def from_mnemonic(
134150
wallet.mnemonic = mnemonic
135151
wallet.index = index
136152
wallet.path = path
153+
wallet.w3 = w3
137154

138155
# modify path if index is provided
139156
if index:

0 commit comments

Comments
 (0)