forked from coccoinomane/web3cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubscribe_controller.py
More file actions
202 lines (191 loc) · 7.64 KB
/
Copy pathsubscribe_controller.py
File metadata and controls
202 lines (191 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import asyncio
import json
from typing import Any
import requests
from cement import ex
from web3.types import TxData
from web3client.types import AsyncSubscriptionCallback, SubscriptionType
from web3cli.exceptions import Web3CliError
from web3cli.framework.controller import Controller
from web3cli.helpers import args
from web3cli.helpers.client_factory import make_client
from web3cli.helpers.render import render
from web3cli.helpers.telegram import send_tg_message
from web3core.helpers.misc import decode_escapes, replace_all
from web3core.helpers.resolve import resolve_address
from web3core.helpers.rpc import check_ws_or_raise
from web3core.helpers.validation import is_valid_url
class SubscribeController(Controller):
"""Handler of the `w3 subscribe` commands"""
class Meta:
label = "subscribe"
help = "Subscribe to stuff happening on the blockchain. Requires a websocket connection. It uses the 'eth_subscribe' RPC method, which is not supported by all chains and nodes. More details here > https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub"
stacked_type = "nested"
stacked_on = "base"
aliases = ["sub"]
@ex(
help="Show new blocks as they are mined. Uses the 'newHeads' subscription.",
arguments=[
*args.subscribe_actions(),
*args.tg_args(),
*args.chain_and_rpc(),
],
aliases=["block", "headers"],
)
def blocks(self) -> None:
check_ws_or_raise(self.app.rpc.url)
self.app.log.info("Subscribing to new blocks, press Ctrl+C to stop...")
asyncio.run(
make_client(self.app).async_subscribe(
on_notification=self.get_callback(),
subscription_type="newHeads",
on_connection_closed=lambda _, __: self.app.log.warning(
"Connection closed, reconnecting..."
),
)
)
@ex(
help="Show new transactions before they are mined. Uses the 'newPendingTransactions' subscription, which is supported only by chains with a mempool.",
arguments=[
args.subscribe_senders(),
*args.subscribe_actions(),
*args.tg_args(),
*args.chain_and_rpc(),
],
aliases=["pending_txs", "txs"],
)
def pending(self) -> None:
check_ws_or_raise(self.app.rpc.url)
self.app.log.info(
"Subscribing to new pending transactions, press Ctrl+C to stop..."
)
asyncio.run(
make_client(self.app).async_subscribe(
on_notification=self.get_callback(),
subscription_type="newPendingTransactions",
tx_from=[
resolve_address(a, chain=self.app.chain.name)
for a in self.app.pargs.senders
],
tx_on_fetch=lambda tx, data: self.app.log.debug(
f"Fetched tx {tx['hash'].hex()} from {tx['from']}"
),
tx_on_fetch_error=lambda e, data: self.app.log.warning(e),
on_connection_closed=lambda _, __: self.app.log.warning(
"Connection closed, reconnecting..."
),
)
)
@ex(
help="Show contract events as they are emitted. Uses the 'logs' subscription.",
arguments=[
(
["--contracts"],
{
"help": "Consider only events emitted by these smart contracts",
"nargs": "+",
"default": [],
},
),
(
["--topics"],
{
"help": "Consider only events with these topics",
"nargs": "+",
"default": [],
},
),
args.subscribe_senders(),
*args.subscribe_actions(),
*args.tg_args(),
*args.chain_and_rpc(),
],
aliases=["logs"],
)
def events(self) -> None:
check_ws_or_raise(self.app.rpc.url)
self.app.log.info("Subscribing to new events, press Ctrl+C to stop...")
asyncio.run(
make_client(self.app).async_subscribe(
on_notification=self.get_callback(),
subscription_type="logs",
logs_addresses=[
resolve_address(a, chain=self.app.chain.name)
for a in self.app.pargs.contracts
],
logs_topics=self.app.pargs.topics,
tx_from=[
resolve_address(a, chain=self.app.chain.name)
for a in self.app.pargs.senders
],
tx_on_fetch=lambda tx, data: self.app.log.debug(
f"Fetched tx {tx['hash'].hex()} from {tx['from']}"
),
tx_on_fetch_error=lambda e, data: self.app.log.warning(e),
on_connection_closed=lambda _, __: self.app.log.warning(
"Connection closed, reconnecting..."
),
)
)
def get_callback(self) -> AsyncSubscriptionCallback:
"""Return the callback to invoke when a notification is received,
based on the command arguments."""
async def callback(data: Any, sub_type: SubscriptionType, tx: TxData) -> None:
# PRINT CALLBACK
if self.app.pargs.print:
render(self.app, data)
# TELEGRAM CALLBACK
if self.app.pargs.telegram:
# Find block number
try:
block = int(
data.get("blockNumber", None) or data.get("number", None), 16
)
except:
block = None
# Find tx hash
try:
tx_hash = data.get("transactionHash", None)
except:
tx_hash = data if type(data) is str else None
# Replace placeholders in the message
msg = replace_all(
self.app.pargs.message,
{
"{data}": json.dumps(data, indent=4),
"{tx}": tx_hash,
"{block}": block,
},
)
# Send the message
send_tg_message(
self.app,
body=decode_escapes(msg),
chat_id=self.app.pargs.telegram
if self.app.pargs.telegram != "config"
else None,
disable_web_page_preview=True,
silent=self.app.pargs.silent,
)
# POST CALLBACK
if self.app.pargs.post:
url = self.app.pargs.post[0]
if not is_valid_url(url):
raise Web3CliError(f"Invalid URL: {url}")
payload = {
"notification_data": data,
"notification_type": sub_type,
"tx_data": tx,
}
response = requests.post(
url=url,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=self.app.get_option("post_callback_timeout"),
)
self.app.log.debug(f"POST callback response: {response.text}")
if response.status_code != 200:
self.app.log.error(
f"POST callback failed with code {response.status_code} and response: {response.text}"
)
return callback