tinybot-eth 0.1.0__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybot-eth
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Minimal Python framework for building crypto bots
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -29,7 +29,14 @@ pip install tinybot-eth
29
29
  | `PRIVATE_KEY` | No | Private key for onchain execution |
30
30
 
31
31
  ## Quick Start
32
+ ```python
33
+ bot = TinyBot(rpc_url, name="my bot")
34
+
35
+ bot.listen(event="AuctionKicked", handler=on_kick, ...)
36
+ bot.every(180, check_expired)
32
37
 
38
+ await bot.run()
39
+ ```
33
40
  ```python
34
41
  import asyncio
35
42
  import os
@@ -60,16 +67,14 @@ async def main():
60
67
  )
61
68
 
62
69
  bot.listen(
63
- name="transfers",
64
70
  event="Transfer",
65
71
  addresses=["0x..."],
66
72
  abi=ERC20_ABI,
67
73
  handler=on_transfer,
68
74
  poll_interval=180,
69
- notify_errors=True,
70
75
  )
71
76
 
72
- bot.every(3600, check_and_tend, notify_errors=True)
77
+ bot.every(3600, check_and_tend)
73
78
 
74
79
  await bot.run()
75
80
 
@@ -97,14 +102,14 @@ Register an event listener.
97
102
 
98
103
  ```python
99
104
  bot.listen(
100
- name="kicks", # unique name
101
105
  event="AuctionKicked", # event name (must exist in ABI)
102
106
  addresses=["0x..."], # contracts to monitor
103
107
  abi=[...], # ABI containing the event
104
108
  handler=on_kick, # async fn(bot, log)
109
+ name="kicks", # defaults to handler.__name__ (optional)
105
110
  poll_interval=180, # seconds between polls (default: 180)
106
111
  block_buffer=5, # re-scan buffer in blocks (default: 5)
107
- notify_errors=True, # send errors to Telegram (default: False)
112
+ notify_errors=True, # send errors to Telegram (default: True)
108
113
  )
109
114
  ```
110
115
 
@@ -115,12 +120,12 @@ The event signature is derived from the ABI at registration time. Raises `ValueE
115
120
 
116
121
  ---
117
122
 
118
- ### `bot.every(interval, handler, name="", notify_errors=False) -> PeriodicTask`
123
+ ### `bot.every(interval, handler, name="", notify_errors=True) -> PeriodicTask`
119
124
 
120
125
  Register a periodic task.
121
126
 
122
127
  ```python
123
- bot.every(3600, check_expired, notify_errors=True)
128
+ bot.every(3600, check_expired)
124
129
  ```
125
130
 
126
131
  Handler signature: `async fn(bot)`
@@ -231,4 +236,4 @@ Access `bot.w3`, `bot.state`, `bot.executor`, and `bot.get_listener()` from any
231
236
 
232
237
  ## Error Handling
233
238
 
234
- When `notify_errors=True`, exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running.
239
+ Enabled by default. Exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running. Set `notify_errors=False` to disable.
@@ -18,7 +18,14 @@ pip install tinybot-eth
18
18
  | `PRIVATE_KEY` | No | Private key for onchain execution |
19
19
 
20
20
  ## Quick Start
21
+ ```python
22
+ bot = TinyBot(rpc_url, name="my bot")
23
+
24
+ bot.listen(event="AuctionKicked", handler=on_kick, ...)
25
+ bot.every(180, check_expired)
21
26
 
27
+ await bot.run()
28
+ ```
22
29
  ```python
23
30
  import asyncio
24
31
  import os
@@ -49,16 +56,14 @@ async def main():
49
56
  )
50
57
 
51
58
  bot.listen(
52
- name="transfers",
53
59
  event="Transfer",
54
60
  addresses=["0x..."],
55
61
  abi=ERC20_ABI,
56
62
  handler=on_transfer,
57
63
  poll_interval=180,
58
- notify_errors=True,
59
64
  )
60
65
 
61
- bot.every(3600, check_and_tend, notify_errors=True)
66
+ bot.every(3600, check_and_tend)
62
67
 
63
68
  await bot.run()
64
69
 
@@ -86,14 +91,14 @@ Register an event listener.
86
91
 
87
92
  ```python
88
93
  bot.listen(
89
- name="kicks", # unique name
90
94
  event="AuctionKicked", # event name (must exist in ABI)
91
95
  addresses=["0x..."], # contracts to monitor
92
96
  abi=[...], # ABI containing the event
93
97
  handler=on_kick, # async fn(bot, log)
98
+ name="kicks", # defaults to handler.__name__ (optional)
94
99
  poll_interval=180, # seconds between polls (default: 180)
95
100
  block_buffer=5, # re-scan buffer in blocks (default: 5)
96
- notify_errors=True, # send errors to Telegram (default: False)
101
+ notify_errors=True, # send errors to Telegram (default: True)
97
102
  )
98
103
  ```
99
104
 
@@ -104,12 +109,12 @@ The event signature is derived from the ABI at registration time. Raises `ValueE
104
109
 
105
110
  ---
106
111
 
107
- ### `bot.every(interval, handler, name="", notify_errors=False) -> PeriodicTask`
112
+ ### `bot.every(interval, handler, name="", notify_errors=True) -> PeriodicTask`
108
113
 
109
114
  Register a periodic task.
110
115
 
111
116
  ```python
112
- bot.every(3600, check_expired, notify_errors=True)
117
+ bot.every(3600, check_expired)
113
118
  ```
114
119
 
115
120
  Handler signature: `async fn(bot)`
@@ -220,4 +225,4 @@ Access `bot.w3`, `bot.state`, `bot.executor`, and `bot.get_listener()` from any
220
225
 
221
226
  ## Error Handling
222
227
 
223
- When `notify_errors=True`, exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running.
228
+ Enabled by default. Exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running. Set `notify_errors=False` to disable.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tinybot-eth"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Minimal Python framework for building crypto bots"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -27,18 +27,19 @@ class TinyBot:
27
27
 
28
28
  def listen(
29
29
  self,
30
- name: str,
31
30
  event: str,
32
31
  addresses: list[str],
33
32
  abi: list[dict[str, Any]],
34
33
  handler: EventHandler,
34
+ name: str = "",
35
35
  poll_interval: int = 180,
36
36
  block_buffer: int = 5,
37
- notify_errors: bool = False,
37
+ notify_errors: bool = True,
38
38
  ) -> EventListener:
39
+ name = name or handler.__name__
39
40
  if not addresses:
40
41
  raise ValueError(f"listener '{name}': addresses cannot be empty")
41
- if any(l.name == name for l in self._listeners):
42
+ if any(listener.name == name for listener in self._listeners):
42
43
  raise ValueError(f"listener '{name}' already registered")
43
44
 
44
45
  signature = event_signature(abi, event)
@@ -62,10 +63,13 @@ class TinyBot:
62
63
  interval: int,
63
64
  handler: TaskHandler,
64
65
  name: str = "",
65
- notify_errors: bool = False,
66
+ notify_errors: bool = True,
66
67
  ) -> PeriodicTask:
68
+ name = name or handler.__name__
69
+ if any(task.name == name for task in self._tasks):
70
+ raise ValueError(f"task '{name}' already registered")
67
71
  task = PeriodicTask(
68
- name=name or handler.__name__,
72
+ name=name,
69
73
  interval=interval,
70
74
  handler=handler,
71
75
  notify_errors=notify_errors,
@@ -78,9 +82,9 @@ class TinyBot:
78
82
  # -------------------------------------------------------------------------
79
83
 
80
84
  def get_listener(self, name: str) -> EventListener:
81
- for l in self._listeners:
82
- if l.name == name:
83
- return l
85
+ for listener in self._listeners:
86
+ if listener.name == name:
87
+ return listener
84
88
  raise ValueError(f"listener '{name}' not found")
85
89
 
86
90
  # -------------------------------------------------------------------------
@@ -107,12 +111,14 @@ class TinyBot:
107
111
  decoder = self.w3.eth.contract(address=listener.addresses[0], abi=listener.abi)
108
112
  event_name = listener.signature.split("(")[0]
109
113
 
110
- raw_logs = self.w3.eth.get_logs({
111
- "fromBlock": from_block,
112
- "toBlock": to_block,
113
- "address": listener.addresses,
114
- "topics": [topic],
115
- })
114
+ raw_logs = self.w3.eth.get_logs(
115
+ {
116
+ "fromBlock": from_block,
117
+ "toBlock": to_block,
118
+ "address": listener.addresses,
119
+ "topics": [topic],
120
+ }
121
+ )
116
122
 
117
123
  for raw_log in raw_logs:
118
124
  event = getattr(decoder.events, event_name)()
@@ -1,6 +1,6 @@
1
- from web3 import Web3
2
1
  from eth_account import Account
3
2
  from eth_account.signers.local import LocalAccount
3
+ from web3 import Web3
4
4
 
5
5
 
6
6
  class Executor:
@@ -24,14 +24,16 @@ class Executor:
24
24
  max_priority_fee_gwei: int = 3,
25
25
  value: int = 0,
26
26
  ) -> str:
27
- tx = call.build_transaction({
28
- "from": self._account.address,
29
- "nonce": self._w3.eth.get_transaction_count(self._account.address),
30
- "gas": gas_limit,
31
- "maxFeePerGas": self._w3.to_wei(max_fee_gwei, "gwei"),
32
- "maxPriorityFeePerGas": self._w3.to_wei(max_priority_fee_gwei, "gwei"),
33
- "value": value,
34
- })
27
+ tx = call.build_transaction(
28
+ {
29
+ "from": self._account.address,
30
+ "nonce": self._w3.eth.get_transaction_count(self._account.address),
31
+ "gas": gas_limit,
32
+ "maxFeePerGas": self._w3.to_wei(max_fee_gwei, "gwei"),
33
+ "maxPriorityFeePerGas": self._w3.to_wei(max_priority_fee_gwei, "gwei"),
34
+ "value": value,
35
+ }
36
+ )
35
37
  signed = self._w3.eth.account.sign_transaction(tx, self._account.key)
36
38
  tx_hash = self._w3.eth.send_raw_transaction(signed.raw_transaction)
37
39
  return tx_hash.hex()
@@ -0,0 +1,46 @@
1
+ from eth_abi import decode as decode_abi
2
+ from web3 import Web3
3
+
4
+ # Multicall3 — same address on all chains
5
+ MULTICALL3 = Web3.to_checksum_address("0xcA11bde05977b3631167028862bE2a173976CA11")
6
+ MULTICALL3_ABI = [
7
+ {
8
+ "name": "aggregate3",
9
+ "type": "function",
10
+ "stateMutability": "payable",
11
+ "inputs": [
12
+ {
13
+ "name": "calls",
14
+ "type": "tuple[]",
15
+ "components": [
16
+ {"name": "target", "type": "address"},
17
+ {"name": "allowFailure", "type": "bool"},
18
+ {"name": "callData", "type": "bytes"},
19
+ ],
20
+ }
21
+ ],
22
+ "outputs": [
23
+ {
24
+ "name": "returnData",
25
+ "type": "tuple[]",
26
+ "components": [
27
+ {"name": "success", "type": "bool"},
28
+ {"name": "returnData", "type": "bytes"},
29
+ ],
30
+ }
31
+ ],
32
+ }
33
+ ]
34
+
35
+
36
+ def multicall(w3: Web3, calls: list) -> list:
37
+ """Batch contract calls. calls = list of ContractFunction objects."""
38
+ mc = w3.eth.contract(address=MULTICALL3, abi=MULTICALL3_ABI)
39
+ encoded = [(call.address, False, call._encode_transaction_data()) for call in calls]
40
+ results = mc.functions.aggregate3(encoded).call()
41
+ decoded = []
42
+ for call, (_, data) in zip(calls, results):
43
+ types = [o["type"] for o in call.abi["outputs"]]
44
+ result = decode_abi(types, data)
45
+ decoded.append(result[0] if len(result) == 1 else result)
46
+ return decoded
@@ -22,7 +22,7 @@ class EventListener:
22
22
  handler: EventHandler
23
23
  poll_interval: int = 180
24
24
  block_buffer: int = 5
25
- notify_errors: bool = False
25
+ notify_errors: bool = True
26
26
  _last_run: float = 0
27
27
  _w3: Web3 | None = None
28
28
 
@@ -42,5 +42,5 @@ class PeriodicTask:
42
42
  name: str
43
43
  interval: int
44
44
  handler: TaskHandler
45
- notify_errors: bool = False
45
+ notify_errors: bool = True
46
46
  _last_run: float = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybot-eth
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Minimal Python framework for building crypto bots
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -29,7 +29,14 @@ pip install tinybot-eth
29
29
  | `PRIVATE_KEY` | No | Private key for onchain execution |
30
30
 
31
31
  ## Quick Start
32
+ ```python
33
+ bot = TinyBot(rpc_url, name="my bot")
34
+
35
+ bot.listen(event="AuctionKicked", handler=on_kick, ...)
36
+ bot.every(180, check_expired)
32
37
 
38
+ await bot.run()
39
+ ```
33
40
  ```python
34
41
  import asyncio
35
42
  import os
@@ -60,16 +67,14 @@ async def main():
60
67
  )
61
68
 
62
69
  bot.listen(
63
- name="transfers",
64
70
  event="Transfer",
65
71
  addresses=["0x..."],
66
72
  abi=ERC20_ABI,
67
73
  handler=on_transfer,
68
74
  poll_interval=180,
69
- notify_errors=True,
70
75
  )
71
76
 
72
- bot.every(3600, check_and_tend, notify_errors=True)
77
+ bot.every(3600, check_and_tend)
73
78
 
74
79
  await bot.run()
75
80
 
@@ -97,14 +102,14 @@ Register an event listener.
97
102
 
98
103
  ```python
99
104
  bot.listen(
100
- name="kicks", # unique name
101
105
  event="AuctionKicked", # event name (must exist in ABI)
102
106
  addresses=["0x..."], # contracts to monitor
103
107
  abi=[...], # ABI containing the event
104
108
  handler=on_kick, # async fn(bot, log)
109
+ name="kicks", # defaults to handler.__name__ (optional)
105
110
  poll_interval=180, # seconds between polls (default: 180)
106
111
  block_buffer=5, # re-scan buffer in blocks (default: 5)
107
- notify_errors=True, # send errors to Telegram (default: False)
112
+ notify_errors=True, # send errors to Telegram (default: True)
108
113
  )
109
114
  ```
110
115
 
@@ -115,12 +120,12 @@ The event signature is derived from the ABI at registration time. Raises `ValueE
115
120
 
116
121
  ---
117
122
 
118
- ### `bot.every(interval, handler, name="", notify_errors=False) -> PeriodicTask`
123
+ ### `bot.every(interval, handler, name="", notify_errors=True) -> PeriodicTask`
119
124
 
120
125
  Register a periodic task.
121
126
 
122
127
  ```python
123
- bot.every(3600, check_expired, notify_errors=True)
128
+ bot.every(3600, check_expired)
124
129
  ```
125
130
 
126
131
  Handler signature: `async fn(bot)`
@@ -231,4 +236,4 @@ Access `bot.w3`, `bot.state`, `bot.executor`, and `bot.get_listener()` from any
231
236
 
232
237
  ## Error Handling
233
238
 
234
- When `notify_errors=True`, exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running.
239
+ Enabled by default. Exceptions are caught and sent to `DEV_GROUP_CHAT_ID` as `[name] error message`. The bot continues running. Set `notify_errors=False` to disable.
@@ -1,32 +0,0 @@
1
- from eth_abi import decode as decode_abi
2
- from web3 import Web3
3
-
4
- # Multicall3 — same address on all chains
5
- MULTICALL3 = Web3.to_checksum_address("0xcA11bde05977b3631167028862bE2a173976CA11")
6
- MULTICALL3_ABI = [{
7
- "name": "aggregate3",
8
- "type": "function",
9
- "stateMutability": "payable",
10
- "inputs": [{"name": "calls", "type": "tuple[]", "components": [
11
- {"name": "target", "type": "address"},
12
- {"name": "allowFailure", "type": "bool"},
13
- {"name": "callData", "type": "bytes"},
14
- ]}],
15
- "outputs": [{"name": "returnData", "type": "tuple[]", "components": [
16
- {"name": "success", "type": "bool"},
17
- {"name": "returnData", "type": "bytes"},
18
- ]}],
19
- }]
20
-
21
-
22
- def multicall(w3: Web3, calls: list) -> list:
23
- """Batch contract calls. calls = list of ContractFunction objects."""
24
- mc = w3.eth.contract(address=MULTICALL3, abi=MULTICALL3_ABI)
25
- encoded = [(call.address, False, call._encode_transaction_data()) for call in calls]
26
- results = mc.functions.aggregate3(encoded).call()
27
- decoded = []
28
- for call, (_, data) in zip(calls, results):
29
- types = [o["type"] for o in call.abi["outputs"]]
30
- result = decode_abi(types, data)
31
- decoded.append(result[0] if len(result) == 1 else result)
32
- return decoded
File without changes
File without changes