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.
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/PKG-INFO +14 -9
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/README.md +13 -8
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/pyproject.toml +1 -1
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/bot.py +20 -14
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/executor.py +11 -9
- tinybot_eth-0.2.0/tinybot/multicall.py +46 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/types.py +2 -2
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot_eth.egg-info/PKG-INFO +14 -9
- tinybot_eth-0.1.0/tinybot/multicall.py +0 -32
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/setup.cfg +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/__init__.py +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/state.py +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/tg.py +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot/utils.py +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot_eth.egg-info/SOURCES.txt +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot_eth.egg-info/dependency_links.txt +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.0}/tinybot_eth.egg-info/requires.txt +0 -0
- {tinybot_eth-0.1.0 → tinybot_eth-0.2.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.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
|
|
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:
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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=
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
82
|
-
if
|
|
83
|
-
return
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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:
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|