nostr-cln-events

cln plugin that publishes events to nostr
git clone git://jb55.com/nostr-cln-events
Log | Files | Refs | README | LICENSE

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()