nostr-events.js (7424B)
1 #!/usr/bin/env node 2 3 import {RelayPool, signId, calculateId, getPublicKey} from 'nostr' 4 import Plugin from 'clightningjs' 5 const plugin = new Plugin() 6 let broadcast 7 8 let forwarded = {} 9 10 async function send_msg(pool, privkey, pubkey, {content, tags}) 11 { 12 const kind = 1 13 const created_at = Math.floor(Date.now()/1000) 14 15 let ev = {pubkey, kind, created_at, content, tags: tags || []} 16 17 ev.id = await calculateId(ev) 18 ev.sig = await signId(privkey, ev.id) 19 20 pool.send(["EVENT", ev]) 21 } 22 23 async function process_forwards(pool, privkey, pubkey) 24 { 25 const stats = Object.keys(forwarded).map(f => { 26 return `${f}: ${forwarded[f]}` 27 }).join("\n") 28 const content = "Forward stats:\n\n" + stats 29 await send_msg(pool, privkey, pubkey, {content}) 30 } 31 32 // process forward notifications every 8 hours 33 //setInterval(process_forwards, 1000 * 60 * 60 * 8) 34 35 let channel_peers = {} 36 37 async function gather_peer_info() { 38 const {peers} = await plugin.rpc.call("listpeers") 39 let nodes = [] 40 for (const peer of peers) { 41 nodes.push(peer.id) 42 for (const channel of peer.channels) { 43 channel_peers[channel.short_channel_id] = peer 44 } 45 } 46 } 47 48 async function get_node(id) { 49 const {nodes} = await plugin.rpc.call("listnodes", {id}) 50 return nodes && nodes[0] 51 } 52 53 function decode(bolt11) { 54 return plugin.rpc.call("decode", {string:bolt11}) 55 } 56 57 async function plugin_init(params) { 58 const relay = params.options['nostr-relay'] 59 const privkey = params.options['nostr-key'] 60 const notify_pk = params.options['nostr-notify-key'] 61 const pubkey = getPublicKey(privkey) 62 let eventList = params.options['nostr-events'] 63 const pool = RelayPool([relay]) 64 65 if (!eventList || eventList === 'all') { 66 eventList = EVENTS 67 } else { 68 eventList = eventList.split(',') 69 } 70 71 await gather_peer_info() 72 73 broadcast = async (params) => { 74 const evName = Object.keys(params)[0] 75 if (!eventList.includes(evName)) return 76 77 let res = null 78 try { 79 res = await handle_msg(notify_pk, evName, params) 80 } catch (e) { 81 await send_msg(pool, privkey, pubkey, { 82 content: `An error was thrown when handling the '${evName}' event: ${e.toString()}` 83 }) 84 return 85 } 86 if (res === null) 87 return 88 if (typeof res === "string") 89 res = {content: res} 90 await send_msg(pool, privkey, pubkey, res) 91 } 92 } 93 94 plugin.onInit = plugin_init 95 96 async function format_sendpay_success(p) 97 { 98 try { 99 const decoded = await decode(p.bolt11) 100 const desc = decoded && decoded.description 101 let dest = p.destination || (decoded && decoded.destination) 102 dest = (dest && await get_node_name(dest)) || dest || "?" 103 return `You sent ${p.amount_msat/1000} sats to ${dest}\n\n${desc}` 104 } catch { 105 } 106 return null 107 } 108 109 async function get_node_name(id) 110 { 111 let mnode 112 try { 113 mnode = await get_node(id) 114 } catch { 115 return id 116 } 117 return mnode ? `'${mnode.alias}'` : id 118 } 119 120 function parse_msats(str) 121 { 122 if (!str) { 123 return 0 124 } 125 126 if (typeof str !== 'string') 127 return str 128 129 return +(str.replace("msat","")) 130 } 131 132 function mksats(str) 133 { 134 return `${parse_msats(str)/1000} sats` 135 } 136 137 function format_coin_movement(p) 138 { 139 const credit = mksats(p.credit) 140 const debit = mksats(p.debit) 141 return `Coins have moved! ${p.type}\n\nCredit: ${credit}\nDebit: ${debit}` 142 } 143 144 function format_warning(p) 145 { 146 if (p.source === "plugin-summary.py") 147 return null 148 return `Warning\n\n${p.log}` 149 } 150 151 async function peer_name(peer) 152 { 153 if (!peer) 154 return null 155 return await get_node_name(peer.id) 156 } 157 158 function format_forward_status(stat) 159 { 160 switch (stat) 161 { 162 case "local_failed": return "Failed to locally forward" 163 case "failed": return "Failed to forward" 164 case "settled": return "Forwarded" 165 } 166 return stat 167 } 168 169 function collect_status(stat) 170 { 171 switch (stat) 172 { 173 case "local_failed": 174 case "failed": return "Failed to collect" 175 case "settled": return "Collected" 176 } 177 return "Collected" 178 } 179 180 async function format_channel_open(p) 181 { 182 const name = await get_node_name(p.id) 183 const sats = mksats(p.amount) 184 return `Incoming channel from ${name} for ${sats}` 185 } 186 187 async function process_forward_event(p) 188 { 189 if (p.status === "failed" || p.status === "local_failed") 190 return null 191 192 //forwarded[p.status] = forwarded[p.status] || {} 193 //let stats = forwarded[p.status] 194 const in_peer = channel_peers[p.in_channel] 195 const out_peer = channel_peers[p.out_channel] 196 const in_name = (await peer_name(in_peer)) || p.in_channel 197 const out_name = (await peer_name(out_peer)) || p.out_channel 198 const in_msats = parse_msats(p.in_msat) 199 const out_msats = parse_msats(p.out_msat) 200 const in_sats = in_msats / 1000.0 201 const out_sats = out_msats / 1000.0 202 const stat = format_forward_status(p.status) 203 const col = collect_status(p.status) 204 if (stat === "offered") 205 return null 206 const collected = (in_msats - out_msats)/1000.0 207 const reason = (p.reason && `Reason: ${p.reason}`) || "" 208 return `${stat}\n\nIn: ${in_name} ${in_sats}\nOut: ${out_name} ${out_sats}\n\n${col} ${collected}sats.\n${reason}` 209 } 210 211 function get_invoice_type(invoice) 212 { 213 return (invoice.bolt11 && "bolt11") || (invoice.bolt12 && "bolt12") || "Unknown" 214 } 215 216 async function process_invoice_payment(notify_pk, params) 217 { 218 const sats = mksats(params.msat) 219 const invoice = await get_invoice(params.label) 220 const content = `You got paid ${sats} for '${invoice.description}'` 221 const tags = [["p", notify_pk]] 222 return {content, tags} 223 } 224 225 async function get_invoice(label) 226 { 227 const {invoices} = await plugin.rpc.call("listinvoices", {label}) 228 return invoices && invoices[0] 229 } 230 231 async function process_invoice_creation(params) 232 { 233 const invoice = await get_invoice(params.label) 234 return `Invoice created: '${invoice.description}'` 235 } 236 237 async function format_channel_state_changed(p) 238 { 239 const node = await get_node_name(p.peer_id) 240 return `${node}'s channel (${p.short_channel_id}) changed from ${p.old_state} to ${p.new_state} caused by ${p.cause}.\n\n"${p.message}"` 241 } 242 243 async function handle_msg(notify_pk, name, params) 244 { 245 if (name === "sendpay_success") 246 return await format_sendpay_success(params.sendpay_success) 247 248 if (name === "coin_movement") 249 return format_coin_movement(params.coin_movement) 250 251 if (name === "channel_opened") 252 return await format_channel_open(params.channel_opened) 253 254 if (name === "channel_state_changed") 255 return await format_channel_state_changed(params.channel_state_changed) 256 257 if (name === "warning") 258 return format_warning(params.warning) 259 260 if (name === "forward_event") 261 return await process_forward_event(params.forward_event) 262 263 if (name === "invoice_payment") 264 return process_invoice_payment(notify_pk, params.invoice_payment) 265 266 if (name === "invoice_creation") 267 return process_invoice_creation(params.invoice_creation) 268 269 return {content:name + ": \n\n" + JSON.stringify(params)} 270 } 271 272 const EVENTS = [ 273 'channel_opened', 274 'channel_open_failed', 275 'channel_state_changed', 276 'invoice_payment', 277 'invoice_creation', 278 'warning', 279 'forward_event', 280 'sendpay_success', 281 'shutdown' 282 ] 283 284 EVENTS.map((ev) => { 285 plugin.subscribe(ev) 286 plugin.notifications[ev].on(ev, (params) => { 287 if (broadcast) { 288 broadcast(params) 289 } 290 }) 291 }) 292 293 plugin.addOption('nostr-relay', 'ws://127.0.0.1:8080', 'nostr relay to send events to', 'string') 294 plugin.addOption('nostr-key', 'hexstr of a 32byte key', 'nostr secret key', 'string') 295 plugin.addOption('nostr-notify-key', 'pubkey to be notified for important events', 'nostr notify pubkey', 'string') 296 plugin.addOption('nostr-events', 'all', 'List of events to broadcast. (Default: broadcast everything)', 'string') 297 298 plugin.start()