-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmwebmonitor.py
More file actions
203 lines (170 loc) · 6.65 KB
/
Copy pathmwebmonitor.py
File metadata and controls
203 lines (170 loc) · 6.65 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
203
"""Watchlist monitor.
Watch public Litecoin addresses; POST a webhook when one funds a peg-in or
receives a peg-out. Run `check` after each analysis pass (cron / systemd
timer): it records new hits and POSTs un-notified ones to each watch's
webhook_url.
Usage:
python3 mwebmonitor.py add <address> [webhook_url] [label]
python3 mwebmonitor.py list
python3 mwebmonitor.py remove <address>
python3 mwebmonitor.py check # default; find + fire notifications
"""
import sqlite3
import sys
import time
import json
try:
import requests
except ImportError:
requests = None
from network import PARAMS as _NET
DB_PATH = _NET['DB_FILENAME']
WEBHOOK_TIMEOUT = 10
def connect():
conn = sqlite3.connect(DB_PATH)
conn.execute('PRAGMA journal_mode = WAL;')
conn.execute('PRAGMA busy_timeout = 5000;')
init(conn)
return conn
def init(conn):
conn.execute('''
CREATE TABLE IF NOT EXISTS watchlist (
address TEXT PRIMARY KEY,
label TEXT,
webhook_url TEXT,
created_at INTEGER
)
''')
conn.execute('''
CREATE TABLE IF NOT EXISTS watch_hits (
address TEXT,
kind TEXT, -- 'pegout_received' or 'pegin_funded'
txid TEXT,
vout INTEGER,
block_height INTEGER,
amount REAL,
notified INTEGER DEFAULT 0,
ts INTEGER,
PRIMARY KEY (address, kind, txid, vout)
)
''')
conn.commit()
def add(conn, address, webhook_url=None, label=None):
conn.execute('''
INSERT INTO watchlist (address, label, webhook_url, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(address) DO UPDATE SET
label=excluded.label, webhook_url=excluded.webhook_url
''', (address, label, webhook_url, int(time.time())))
conn.commit()
print(f"Watching {address}" + (f" -> {webhook_url}" if webhook_url else " (no webhook)"))
def remove(conn, address):
cur = conn.execute('DELETE FROM watchlist WHERE address = ?', (address,))
conn.commit()
print(f"Removed {address}" if cur.rowcount else f"{address} was not watched")
def list_watches(conn):
rows = conn.execute('SELECT address, label, webhook_url FROM watchlist ORDER BY created_at').fetchall()
if not rows:
print("Watchlist is empty.")
return
for address, label, webhook in rows:
print(f" {address} label={label or '-'} webhook={webhook or '-'}")
def find_hits(conn):
"""Record any peg activity touching watched addresses. Returns new-hit count."""
cur = conn.cursor()
watches = cur.execute('SELECT address FROM watchlist').fetchall()
new_hits = 0
now = int(time.time())
for (address,) in watches:
# Peg-outs received by the watched address.
for txid, vout, height, amount in cur.execute(
'SELECT txid, vout, block_height, amount FROM mweb_pegouts WHERE address = ?',
(address,)
).fetchall():
cur.execute('''
INSERT OR IGNORE INTO watch_hits
(address, kind, txid, vout, block_height, amount, notified, ts)
VALUES (?, 'pegout_received', ?, ?, ?, ?, 0, ?)
''', (address, txid, vout, height, amount, now))
new_hits += cur.rowcount
# Peg-ins funded by the watched address: as the dominant source or as
# any one of the tx inputs (common-input ownership).
for txid, vout, height, amount in cur.execute(
'''SELECT DISTINCT p.txid, p.vout, p.block_height, p.amount
FROM mweb_pegins p
WHERE p.source_address = ?
OR p.txid IN (SELECT pegin_txid FROM pegin_inputs WHERE address = ?)''',
(address, address)
).fetchall():
# Use the real vout so multiple peg-in outputs in one tx get
# distinct (address, kind, txid, vout) PKs instead of colliding
# under INSERT OR IGNORE.
cur.execute('''
INSERT OR IGNORE INTO watch_hits
(address, kind, txid, vout, block_height, amount, notified, ts)
VALUES (?, 'pegin_funded', ?, ?, ?, ?, 0, ?)
''', (address, txid, vout, height, amount, now))
new_hits += cur.rowcount
conn.commit()
return new_hits
def notify(conn):
"""POST un-notified hits to each watch's webhook. Returns sent count."""
cur = conn.cursor()
pending = cur.execute('''
SELECT h.address, h.kind, h.txid, h.vout, h.block_height, h.amount,
w.webhook_url, w.label
FROM watch_hits h
JOIN watchlist w ON w.address = h.address
WHERE h.notified = 0 AND w.webhook_url IS NOT NULL AND w.webhook_url != ''
''').fetchall()
sent = 0
for address, kind, txid, vout, height, amount, webhook, label in pending:
payload = {
'address': address, 'label': label, 'kind': kind,
'txid': txid, 'vout': vout, 'block_height': height, 'amount': amount,
}
if requests is None:
print(" requests not installed; cannot POST webhooks")
break
try:
r = requests.post(webhook, json=payload, timeout=WEBHOOK_TIMEOUT)
r.raise_for_status() # 4xx/5xx: stay notified=0, retry next run
cur.execute('''
UPDATE watch_hits SET notified = 1
WHERE address = ? AND kind = ? AND txid = ? AND vout = ?
''', (address, kind, txid, vout))
conn.commit() # persist per POST so a crash can't re-fire it
sent += 1
except Exception as e:
print(f" webhook failed for {address} ({txid}): {e}")
# Mark webhook-less hits notified so they don't linger as pending.
cur.execute('''
UPDATE watch_hits SET notified = 1
WHERE notified = 0 AND address IN (
SELECT address FROM watchlist WHERE webhook_url IS NULL OR webhook_url = ''
)
''')
conn.commit()
return sent
def check(conn):
new_hits = find_hits(conn)
sent = notify(conn)
print(f"Found {new_hits} new hit(s); sent {sent} webhook notification(s).")
def main():
conn = connect()
cmd = sys.argv[1] if len(sys.argv) > 1 else 'check'
if cmd == 'add' and len(sys.argv) >= 3:
add(conn, sys.argv[2],
sys.argv[3] if len(sys.argv) > 3 else None,
sys.argv[4] if len(sys.argv) > 4 else None)
elif cmd == 'remove' and len(sys.argv) >= 3:
remove(conn, sys.argv[2])
elif cmd == 'list':
list_watches(conn)
elif cmd == 'check':
check(conn)
else:
print(__doc__)
conn.close()
if __name__ == '__main__':
main()