tinybot-eth 0.6.0__tar.gz → 0.7.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.
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/PKG-INFO +27 -8
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/README.md +26 -7
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/pyproject.toml +1 -1
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/__init__.py +2 -1
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/bot.py +4 -3
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/executor.py +13 -4
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/multicall.py +23 -8
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/tg.py +7 -9
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot_eth.egg-info/PKG-INFO +27 -8
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/setup.cfg +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/state.py +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/types.py +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot/utils.py +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot_eth.egg-info/SOURCES.txt +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot_eth.egg-info/dependency_links.txt +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot_eth.egg-info/requires.txt +0 -0
- {tinybot_eth-0.6.0 → tinybot_eth-0.7.0}/tinybot_eth.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybot-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Minimal Python framework for building crypto bots
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -24,9 +24,9 @@ pip install tinybot-eth
|
|
|
24
24
|
|
|
25
25
|
| Variable | Required | Description |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| `BOT_ACCESS_TOKEN` |
|
|
28
|
-
| `GROUP_CHAT_ID` |
|
|
29
|
-
| `DEV_GROUP_CHAT_ID` |
|
|
27
|
+
| `BOT_ACCESS_TOKEN` | No | Telegram bot token. Unset: `notify_group_chat` does nothing and `telegram_enabled()` is `False` |
|
|
28
|
+
| `GROUP_CHAT_ID` | No | Telegram group for notifications |
|
|
29
|
+
| `DEV_GROUP_CHAT_ID` | No | Telegram group for errors and startup |
|
|
30
30
|
| `PRIVATE_KEY` | No | Private key for onchain execution |
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
@@ -85,7 +85,7 @@ asyncio.run(main())
|
|
|
85
85
|
|
|
86
86
|
## API
|
|
87
87
|
|
|
88
|
-
### `TinyBot(rpc_url, name="tinybot", private_key="")`
|
|
88
|
+
### `TinyBot(rpc_url, private_rpc_url="", name="tinybot", private_key="")`
|
|
89
89
|
|
|
90
90
|
Creates a bot instance.
|
|
91
91
|
|
|
@@ -93,6 +93,7 @@ Creates a bot instance.
|
|
|
93
93
|
- `bot.state` — `State` instance (see below)
|
|
94
94
|
- `bot.executor` — `Executor` instance if `private_key` is provided, else `None`
|
|
95
95
|
- `bot.name` — used in logs and Telegram startup message
|
|
96
|
+
- `private_rpc_url` — optional private relay (e.g. `https://rpc.flashbots.net/fast`), used only for txs sent with `execute(..., private=True)`; reads and public txs stay on `rpc_url`
|
|
96
97
|
|
|
97
98
|
On `run()`, sends a startup message to `DEV_GROUP_CHAT_ID` and prints a polling heartbeat every tick.
|
|
98
99
|
|
|
@@ -194,16 +195,20 @@ tx_hash = bot.executor.execute(
|
|
|
194
195
|
max_priority_fee_gwei=0, # default: 0
|
|
195
196
|
simulate=True, # default: True — dry-run via eth_call before sending
|
|
196
197
|
wait=120, # default: 120 — seconds to wait for mining (0 = fire and forget)
|
|
198
|
+
private=False, # default: False — True broadcasts via private_rpc_url
|
|
199
|
+
replace_pending=False, # default: False — True reuses the nonce of an unmined tx to replace it
|
|
197
200
|
)
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
- `executor.address` — signer address
|
|
201
204
|
- `executor.balance` — signer ETH balance in wei
|
|
202
|
-
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns tx hash
|
|
205
|
+
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns the `0x`-prefixed tx hash
|
|
203
206
|
- `gas_limit=0` (default) — auto-estimates gas with 1.5x buffer; pass a value to override
|
|
204
207
|
- `max_fee_gwei` / `max_priority_fee_gwei` — accept floats (e.g. `0.1`)
|
|
205
208
|
- `simulate=True` (default) — runs `call.call()` first; reverts raise before the tx is sent
|
|
206
209
|
- `wait=120` (default) — wait up to N seconds for the tx to be mined; `0` for fire and forget
|
|
210
|
+
- `replace_pending=False` (default) — nonce from the `pending` block, so a tx sent while an earlier one is unmined queues behind it; `replace_pending=True` — nonce from the `latest` block, so the tx replaces the unmined one (the node requires a ~10% higher fee)
|
|
211
|
+
- `private=False` (default) — broadcasts via `rpc_url`; `private=True` broadcasts via `private_rpc_url` and raises if it is not set. There is no fallback between the two, so a private tx never goes public
|
|
207
212
|
|
|
208
213
|
---
|
|
209
214
|
|
|
@@ -220,7 +225,7 @@ In-memory state, available via `bot.state`.
|
|
|
220
225
|
|
|
221
226
|
---
|
|
222
227
|
|
|
223
|
-
### `multicall(w3, calls) -> list`
|
|
228
|
+
### `multicall(w3, calls, allow_failure=False, batch_size=200) -> list`
|
|
224
229
|
|
|
225
230
|
Batch contract reads via [Multicall3](https://github.com/mds1/multicall).
|
|
226
231
|
|
|
@@ -229,13 +234,27 @@ symbol, decimals = multicall(bot.w3, [
|
|
|
229
234
|
token.functions.symbol(),
|
|
230
235
|
token.functions.decimals(),
|
|
231
236
|
])
|
|
237
|
+
|
|
238
|
+
# one result per call; None where the call reverted or returned nothing
|
|
239
|
+
keepers = multicall(bot.w3, [s.functions.keeper() for s in strategies], allow_failure=True)
|
|
232
240
|
```
|
|
233
241
|
|
|
242
|
+
- `allow_failure=False` (default) — any reverting call reverts the whole batch
|
|
243
|
+
- `allow_failure=True` — a reverting call, empty return data, or undecodable data yields `None`
|
|
244
|
+
- `batch_size=200` — calls are split into chunks of this size to stay under RPC `eth_call` gas caps
|
|
245
|
+
- Functions returning tuples/structs are decoded
|
|
246
|
+
|
|
234
247
|
---
|
|
235
248
|
|
|
236
249
|
### `notify_group_chat(text, parse_mode="HTML", chat_id=GROUP_CHAT_ID)`
|
|
237
250
|
|
|
238
|
-
Send a Telegram message. HTML parse mode by default.
|
|
251
|
+
Send a Telegram message. HTML parse mode by default. Does nothing when `BOT_ACCESS_TOKEN` or the chat id is unset.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### `telegram_enabled() -> bool`
|
|
256
|
+
|
|
257
|
+
`True` when `BOT_ACCESS_TOKEN` is set. Use it to skip work whose only output is a Telegram message.
|
|
239
258
|
|
|
240
259
|
---
|
|
241
260
|
|
|
@@ -12,9 +12,9 @@ pip install tinybot-eth
|
|
|
12
12
|
|
|
13
13
|
| Variable | Required | Description |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| `BOT_ACCESS_TOKEN` |
|
|
16
|
-
| `GROUP_CHAT_ID` |
|
|
17
|
-
| `DEV_GROUP_CHAT_ID` |
|
|
15
|
+
| `BOT_ACCESS_TOKEN` | No | Telegram bot token. Unset: `notify_group_chat` does nothing and `telegram_enabled()` is `False` |
|
|
16
|
+
| `GROUP_CHAT_ID` | No | Telegram group for notifications |
|
|
17
|
+
| `DEV_GROUP_CHAT_ID` | No | Telegram group for errors and startup |
|
|
18
18
|
| `PRIVATE_KEY` | No | Private key for onchain execution |
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
@@ -73,7 +73,7 @@ asyncio.run(main())
|
|
|
73
73
|
|
|
74
74
|
## API
|
|
75
75
|
|
|
76
|
-
### `TinyBot(rpc_url, name="tinybot", private_key="")`
|
|
76
|
+
### `TinyBot(rpc_url, private_rpc_url="", name="tinybot", private_key="")`
|
|
77
77
|
|
|
78
78
|
Creates a bot instance.
|
|
79
79
|
|
|
@@ -81,6 +81,7 @@ Creates a bot instance.
|
|
|
81
81
|
- `bot.state` — `State` instance (see below)
|
|
82
82
|
- `bot.executor` — `Executor` instance if `private_key` is provided, else `None`
|
|
83
83
|
- `bot.name` — used in logs and Telegram startup message
|
|
84
|
+
- `private_rpc_url` — optional private relay (e.g. `https://rpc.flashbots.net/fast`), used only for txs sent with `execute(..., private=True)`; reads and public txs stay on `rpc_url`
|
|
84
85
|
|
|
85
86
|
On `run()`, sends a startup message to `DEV_GROUP_CHAT_ID` and prints a polling heartbeat every tick.
|
|
86
87
|
|
|
@@ -182,16 +183,20 @@ tx_hash = bot.executor.execute(
|
|
|
182
183
|
max_priority_fee_gwei=0, # default: 0
|
|
183
184
|
simulate=True, # default: True — dry-run via eth_call before sending
|
|
184
185
|
wait=120, # default: 120 — seconds to wait for mining (0 = fire and forget)
|
|
186
|
+
private=False, # default: False — True broadcasts via private_rpc_url
|
|
187
|
+
replace_pending=False, # default: False — True reuses the nonce of an unmined tx to replace it
|
|
185
188
|
)
|
|
186
189
|
```
|
|
187
190
|
|
|
188
191
|
- `executor.address` — signer address
|
|
189
192
|
- `executor.balance` — signer ETH balance in wei
|
|
190
|
-
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns tx hash
|
|
193
|
+
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns the `0x`-prefixed tx hash
|
|
191
194
|
- `gas_limit=0` (default) — auto-estimates gas with 1.5x buffer; pass a value to override
|
|
192
195
|
- `max_fee_gwei` / `max_priority_fee_gwei` — accept floats (e.g. `0.1`)
|
|
193
196
|
- `simulate=True` (default) — runs `call.call()` first; reverts raise before the tx is sent
|
|
194
197
|
- `wait=120` (default) — wait up to N seconds for the tx to be mined; `0` for fire and forget
|
|
198
|
+
- `replace_pending=False` (default) — nonce from the `pending` block, so a tx sent while an earlier one is unmined queues behind it; `replace_pending=True` — nonce from the `latest` block, so the tx replaces the unmined one (the node requires a ~10% higher fee)
|
|
199
|
+
- `private=False` (default) — broadcasts via `rpc_url`; `private=True` broadcasts via `private_rpc_url` and raises if it is not set. There is no fallback between the two, so a private tx never goes public
|
|
195
200
|
|
|
196
201
|
---
|
|
197
202
|
|
|
@@ -208,7 +213,7 @@ In-memory state, available via `bot.state`.
|
|
|
208
213
|
|
|
209
214
|
---
|
|
210
215
|
|
|
211
|
-
### `multicall(w3, calls) -> list`
|
|
216
|
+
### `multicall(w3, calls, allow_failure=False, batch_size=200) -> list`
|
|
212
217
|
|
|
213
218
|
Batch contract reads via [Multicall3](https://github.com/mds1/multicall).
|
|
214
219
|
|
|
@@ -217,13 +222,27 @@ symbol, decimals = multicall(bot.w3, [
|
|
|
217
222
|
token.functions.symbol(),
|
|
218
223
|
token.functions.decimals(),
|
|
219
224
|
])
|
|
225
|
+
|
|
226
|
+
# one result per call; None where the call reverted or returned nothing
|
|
227
|
+
keepers = multicall(bot.w3, [s.functions.keeper() for s in strategies], allow_failure=True)
|
|
220
228
|
```
|
|
221
229
|
|
|
230
|
+
- `allow_failure=False` (default) — any reverting call reverts the whole batch
|
|
231
|
+
- `allow_failure=True` — a reverting call, empty return data, or undecodable data yields `None`
|
|
232
|
+
- `batch_size=200` — calls are split into chunks of this size to stay under RPC `eth_call` gas caps
|
|
233
|
+
- Functions returning tuples/structs are decoded
|
|
234
|
+
|
|
222
235
|
---
|
|
223
236
|
|
|
224
237
|
### `notify_group_chat(text, parse_mode="HTML", chat_id=GROUP_CHAT_ID)`
|
|
225
238
|
|
|
226
|
-
Send a Telegram message. HTML parse mode by default.
|
|
239
|
+
Send a Telegram message. HTML parse mode by default. Does nothing when `BOT_ACCESS_TOKEN` or the chat id is unset.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### `telegram_enabled() -> bool`
|
|
244
|
+
|
|
245
|
+
`True` when `BOT_ACCESS_TOKEN` is set. Use it to skip work whose only output is a Telegram message.
|
|
227
246
|
|
|
228
247
|
---
|
|
229
248
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from tinybot.bot import TinyBot
|
|
2
2
|
from tinybot.executor import Executor
|
|
3
3
|
from tinybot.multicall import multicall
|
|
4
|
-
from tinybot.tg import DEV_GROUP_CHAT_ID, notify_group_chat
|
|
4
|
+
from tinybot.tg import DEV_GROUP_CHAT_ID, notify_group_chat, telegram_enabled
|
|
5
5
|
from tinybot.utils import event_id
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
@@ -11,4 +11,5 @@ __all__ = [
|
|
|
11
11
|
"event_id",
|
|
12
12
|
"multicall",
|
|
13
13
|
"notify_group_chat",
|
|
14
|
+
"telegram_enabled",
|
|
14
15
|
]
|
|
@@ -15,11 +15,12 @@ from tinybot.utils import event_id, event_signature
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class TinyBot:
|
|
18
|
-
def __init__(self, rpc_url: str, name: str = "tinybot", private_key: str = ""):
|
|
19
|
-
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
18
|
+
def __init__(self, rpc_url: str, private_rpc_url: str = "", name: str = "tinybot", private_key: str = ""):
|
|
19
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url, cache_allowed_requests=True, cacheable_requests={"eth_chainId"}))
|
|
20
20
|
self.name = name
|
|
21
21
|
self.state = State()
|
|
22
|
-
self.
|
|
22
|
+
self._private_w3 = Web3(Web3.HTTPProvider(private_rpc_url)) if private_rpc_url else None
|
|
23
|
+
self.executor = Executor(self.w3, private_key, self._private_w3) if private_key else None
|
|
23
24
|
self._listeners: list[EventListener] = []
|
|
24
25
|
self._tasks: list[PeriodicTask] = []
|
|
25
26
|
self._crons: list[tuple[CronTask, croniter]] = []
|
|
@@ -4,8 +4,9 @@ from web3 import Web3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Executor:
|
|
7
|
-
def __init__(self, w3: Web3, private_key: str):
|
|
7
|
+
def __init__(self, w3: Web3, private_key: str, private_w3: Web3 | None = None):
|
|
8
8
|
self._w3 = w3
|
|
9
|
+
self._private_w3 = private_w3 # optional private relay, used only for txs sent with `private=True`
|
|
9
10
|
self._account: LocalAccount = Account.from_key(private_key)
|
|
10
11
|
|
|
11
12
|
@property
|
|
@@ -25,7 +26,12 @@ class Executor:
|
|
|
25
26
|
value: int = 0,
|
|
26
27
|
simulate: bool = True,
|
|
27
28
|
wait: int = 120,
|
|
29
|
+
private: bool = False,
|
|
30
|
+
replace_pending: bool = False,
|
|
28
31
|
) -> str:
|
|
32
|
+
if private and self._private_w3 is None:
|
|
33
|
+
raise ValueError("private tx requested but no private_rpc_url is set")
|
|
34
|
+
|
|
29
35
|
if simulate:
|
|
30
36
|
call.call({"from": self._account.address, "value": value})
|
|
31
37
|
|
|
@@ -35,7 +41,9 @@ class Executor:
|
|
|
35
41
|
tx = call.build_transaction(
|
|
36
42
|
{
|
|
37
43
|
"from": self._account.address,
|
|
38
|
-
"nonce": self._w3.eth.get_transaction_count(
|
|
44
|
+
"nonce": self._w3.eth.get_transaction_count(
|
|
45
|
+
self._account.address, "latest" if replace_pending else "pending"
|
|
46
|
+
),
|
|
39
47
|
"gas": gas_limit,
|
|
40
48
|
"maxFeePerGas": self._w3.to_wei(max_fee_gwei, "gwei"),
|
|
41
49
|
"maxPriorityFeePerGas": self._w3.to_wei(max_priority_fee_gwei, "gwei"),
|
|
@@ -43,7 +51,8 @@ class Executor:
|
|
|
43
51
|
}
|
|
44
52
|
)
|
|
45
53
|
signed = self._w3.eth.account.sign_transaction(tx, self._account.key)
|
|
46
|
-
|
|
54
|
+
send_w3 = self._private_w3 if private else self._w3
|
|
55
|
+
tx_hash = send_w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
47
56
|
if wait > 0:
|
|
48
57
|
self._w3.eth.wait_for_transaction_receipt(tx_hash, timeout=wait)
|
|
49
|
-
return tx_hash.
|
|
58
|
+
return tx_hash.to_0x_hex()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from eth_abi import decode as decode_abi
|
|
2
|
+
from eth_utils import get_abi_output_types
|
|
2
3
|
from web3 import Web3
|
|
3
4
|
|
|
4
5
|
# Multicall3 — same address on all chains
|
|
@@ -33,14 +34,28 @@ MULTICALL3_ABI = [
|
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def multicall(w3: Web3, calls: list) -> list:
|
|
37
|
-
"""Batch contract calls. calls = list of ContractFunction objects.
|
|
37
|
+
def multicall(w3: Web3, calls: list, allow_failure: bool = False, batch_size: int = 200) -> list:
|
|
38
|
+
"""Batch contract calls. calls = list of ContractFunction objects.
|
|
39
|
+
|
|
40
|
+
With allow_failure=True, a reverting call (or one that returns no data) yields None instead of
|
|
41
|
+
reverting the whole batch. Calls are sent in chunks of batch_size to stay under eth_call gas caps.
|
|
42
|
+
"""
|
|
38
43
|
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
44
|
decoded = []
|
|
42
|
-
for
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
for i in range(0, len(calls), batch_size):
|
|
46
|
+
chunk = calls[i : i + batch_size]
|
|
47
|
+
encoded = [(call.address, allow_failure, call._encode_transaction_data()) for call in chunk]
|
|
48
|
+
results = mc.functions.aggregate3(encoded).call()
|
|
49
|
+
for call, (success, data) in zip(chunk, results):
|
|
50
|
+
if allow_failure and (not success or not data):
|
|
51
|
+
decoded.append(None)
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
result = decode_abi(get_abi_output_types(call.abi), data)
|
|
55
|
+
except Exception:
|
|
56
|
+
if not allow_failure:
|
|
57
|
+
raise
|
|
58
|
+
decoded.append(None)
|
|
59
|
+
continue
|
|
60
|
+
decoded.append(result[0] if len(result) == 1 else result)
|
|
46
61
|
return decoded
|
|
@@ -2,17 +2,13 @@ import os
|
|
|
2
2
|
|
|
3
3
|
from telegram import Bot
|
|
4
4
|
|
|
5
|
+
BOT_ACCESS_TOKEN = os.getenv("BOT_ACCESS_TOKEN", "")
|
|
6
|
+
GROUP_CHAT_ID = int(os.getenv("GROUP_CHAT_ID", "0"))
|
|
7
|
+
DEV_GROUP_CHAT_ID = int(os.getenv("DEV_GROUP_CHAT_ID", "0"))
|
|
5
8
|
|
|
6
|
-
def _require_env(name: str) -> str:
|
|
7
|
-
val = os.getenv(name, "")
|
|
8
|
-
if not val:
|
|
9
|
-
raise RuntimeError(f"!{name}")
|
|
10
|
-
return val
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
GROUP_CHAT_ID = int(_require_env("GROUP_CHAT_ID"))
|
|
15
|
-
DEV_GROUP_CHAT_ID = int(_require_env("DEV_GROUP_CHAT_ID"))
|
|
10
|
+
def telegram_enabled() -> bool:
|
|
11
|
+
return bool(BOT_ACCESS_TOKEN)
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
async def notify_group_chat(
|
|
@@ -21,6 +17,8 @@ async def notify_group_chat(
|
|
|
21
17
|
chat_id: int = GROUP_CHAT_ID,
|
|
22
18
|
disable_web_page_preview: bool = True,
|
|
23
19
|
) -> None:
|
|
20
|
+
if not BOT_ACCESS_TOKEN or not chat_id:
|
|
21
|
+
return
|
|
24
22
|
try:
|
|
25
23
|
bot = Bot(token=BOT_ACCESS_TOKEN)
|
|
26
24
|
await bot.send_message(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinybot-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Minimal Python framework for building crypto bots
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -24,9 +24,9 @@ pip install tinybot-eth
|
|
|
24
24
|
|
|
25
25
|
| Variable | Required | Description |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| `BOT_ACCESS_TOKEN` |
|
|
28
|
-
| `GROUP_CHAT_ID` |
|
|
29
|
-
| `DEV_GROUP_CHAT_ID` |
|
|
27
|
+
| `BOT_ACCESS_TOKEN` | No | Telegram bot token. Unset: `notify_group_chat` does nothing and `telegram_enabled()` is `False` |
|
|
28
|
+
| `GROUP_CHAT_ID` | No | Telegram group for notifications |
|
|
29
|
+
| `DEV_GROUP_CHAT_ID` | No | Telegram group for errors and startup |
|
|
30
30
|
| `PRIVATE_KEY` | No | Private key for onchain execution |
|
|
31
31
|
|
|
32
32
|
## Quick Start
|
|
@@ -85,7 +85,7 @@ asyncio.run(main())
|
|
|
85
85
|
|
|
86
86
|
## API
|
|
87
87
|
|
|
88
|
-
### `TinyBot(rpc_url, name="tinybot", private_key="")`
|
|
88
|
+
### `TinyBot(rpc_url, private_rpc_url="", name="tinybot", private_key="")`
|
|
89
89
|
|
|
90
90
|
Creates a bot instance.
|
|
91
91
|
|
|
@@ -93,6 +93,7 @@ Creates a bot instance.
|
|
|
93
93
|
- `bot.state` — `State` instance (see below)
|
|
94
94
|
- `bot.executor` — `Executor` instance if `private_key` is provided, else `None`
|
|
95
95
|
- `bot.name` — used in logs and Telegram startup message
|
|
96
|
+
- `private_rpc_url` — optional private relay (e.g. `https://rpc.flashbots.net/fast`), used only for txs sent with `execute(..., private=True)`; reads and public txs stay on `rpc_url`
|
|
96
97
|
|
|
97
98
|
On `run()`, sends a startup message to `DEV_GROUP_CHAT_ID` and prints a polling heartbeat every tick.
|
|
98
99
|
|
|
@@ -194,16 +195,20 @@ tx_hash = bot.executor.execute(
|
|
|
194
195
|
max_priority_fee_gwei=0, # default: 0
|
|
195
196
|
simulate=True, # default: True — dry-run via eth_call before sending
|
|
196
197
|
wait=120, # default: 120 — seconds to wait for mining (0 = fire and forget)
|
|
198
|
+
private=False, # default: False — True broadcasts via private_rpc_url
|
|
199
|
+
replace_pending=False, # default: False — True reuses the nonce of an unmined tx to replace it
|
|
197
200
|
)
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
- `executor.address` — signer address
|
|
201
204
|
- `executor.balance` — signer ETH balance in wei
|
|
202
|
-
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns tx hash
|
|
205
|
+
- `executor.execute(call, ...)` — sign and broadcast a transaction, returns the `0x`-prefixed tx hash
|
|
203
206
|
- `gas_limit=0` (default) — auto-estimates gas with 1.5x buffer; pass a value to override
|
|
204
207
|
- `max_fee_gwei` / `max_priority_fee_gwei` — accept floats (e.g. `0.1`)
|
|
205
208
|
- `simulate=True` (default) — runs `call.call()` first; reverts raise before the tx is sent
|
|
206
209
|
- `wait=120` (default) — wait up to N seconds for the tx to be mined; `0` for fire and forget
|
|
210
|
+
- `replace_pending=False` (default) — nonce from the `pending` block, so a tx sent while an earlier one is unmined queues behind it; `replace_pending=True` — nonce from the `latest` block, so the tx replaces the unmined one (the node requires a ~10% higher fee)
|
|
211
|
+
- `private=False` (default) — broadcasts via `rpc_url`; `private=True` broadcasts via `private_rpc_url` and raises if it is not set. There is no fallback between the two, so a private tx never goes public
|
|
207
212
|
|
|
208
213
|
---
|
|
209
214
|
|
|
@@ -220,7 +225,7 @@ In-memory state, available via `bot.state`.
|
|
|
220
225
|
|
|
221
226
|
---
|
|
222
227
|
|
|
223
|
-
### `multicall(w3, calls) -> list`
|
|
228
|
+
### `multicall(w3, calls, allow_failure=False, batch_size=200) -> list`
|
|
224
229
|
|
|
225
230
|
Batch contract reads via [Multicall3](https://github.com/mds1/multicall).
|
|
226
231
|
|
|
@@ -229,13 +234,27 @@ symbol, decimals = multicall(bot.w3, [
|
|
|
229
234
|
token.functions.symbol(),
|
|
230
235
|
token.functions.decimals(),
|
|
231
236
|
])
|
|
237
|
+
|
|
238
|
+
# one result per call; None where the call reverted or returned nothing
|
|
239
|
+
keepers = multicall(bot.w3, [s.functions.keeper() for s in strategies], allow_failure=True)
|
|
232
240
|
```
|
|
233
241
|
|
|
242
|
+
- `allow_failure=False` (default) — any reverting call reverts the whole batch
|
|
243
|
+
- `allow_failure=True` — a reverting call, empty return data, or undecodable data yields `None`
|
|
244
|
+
- `batch_size=200` — calls are split into chunks of this size to stay under RPC `eth_call` gas caps
|
|
245
|
+
- Functions returning tuples/structs are decoded
|
|
246
|
+
|
|
234
247
|
---
|
|
235
248
|
|
|
236
249
|
### `notify_group_chat(text, parse_mode="HTML", chat_id=GROUP_CHAT_ID)`
|
|
237
250
|
|
|
238
|
-
Send a Telegram message. HTML parse mode by default.
|
|
251
|
+
Send a Telegram message. HTML parse mode by default. Does nothing when `BOT_ACCESS_TOKEN` or the chat id is unset.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### `telegram_enabled() -> bool`
|
|
256
|
+
|
|
257
|
+
`True` when `BOT_ACCESS_TOKEN` is set. Use it to skip work whose only output is a Telegram message.
|
|
239
258
|
|
|
240
259
|
---
|
|
241
260
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|