Files
plugins/currencyrate/currencyrate.py
2021-01-14 18:35:49 +01:00

137 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
from pyln.client import Plugin
from collections import namedtuple
from pyln.client import Millisatoshi
from cachetools import cached, TTLCache
import requests
import statistics
import time
plugin = Plugin()
Source = namedtuple('Source', ['name', 'urlformat', 'replymembers'])
sources = [
# e.g. {"GBP": {"volume_btc": "24.36647424", "rates": {"last": "13667.63"}, "avg_1h": "13786.86", "avg_6h": "13723.65", "avg_12h": "13680.23", "avg_24h": "13739.56"}, "USD": {"volume_btc": "27.97517017", "rates": {"last": "18204.21"}, "avg_1h": "19349.46", "avg_6h": "18621.72", "avg_12h": "18642.28", "avg_24h": "18698.94"}
# ...
# "GTQ": {"volume_btc": "0.03756101", "rates": {"last": "148505.46"}, "avg_1h": "148505.46", "avg_6h": "162463.69", "avg_12h": "162003.10", "avg_24h": "162003.10"}, "DKK": {"volume_btc": "0.00339923", "rates": {"last": "139737.53"}, "avg_12h": "139737.53", "avg_24h": "139737.53"}, "HTG": {"volume_btc": "0.00024758", "rates": {"last": "2019549.24"}, "avg_6h": "2019549.24", "avg_12h": "2019549.24", "avg_24h": "2019549.24"}, "NAD": {"volume_btc": "0.00722222", "rates": {"last": "360000.11"}, "avg_12h": "360000.11", "avg_24h": "360000.11"}}
Source('localbitcoins',
'https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/',
['{currency}', "avg_6h"]),
# e.g. {"high": "18502.56", "last": "17970.41", "timestamp": "1607650787", "bid": "17961.87", "vwap": "18223.42", "volume": "7055.63066541", "low": "17815.92", "ask": "17970.41", "open": "18250.30"}
Source('bitstamp',
'https://www.bitstamp.net/api/v2/ticker/btc{currency_lc}/',
['last']),
# e.g. {"bitcoin":{"usd":17885.84}}
Source('coingecko',
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies={currency_lc}',
['bitcoin', '{currency_lc}']),
]
def get_currencyrate(plugin, currency, req_template, response_members):
# NOTE: Bitstamp has a DNS/Proxy issues that can return 404
# Workaround: retry up to 5 times with a delay
currency_lc = currency.lower()
url = req_template.format(currency_lc=currency_lc, currency=currency)
for _ in range(5):
r = requests.get(url, proxies=plugin.proxies)
if r.status_code != 200:
time.sleep(1)
continue
break
if r.status_code != 200:
plugin.log(level='info', message='{}: bad response {}'.format(url, r.status_code))
return None
json = r.json()
for m in response_members:
expanded = m.format(currency_lc=currency_lc, currency=currency)
if expanded not in json:
plugin.log(level='debug', message='{}: {} not in {}'.format(url, expanded, json))
return None
json = json[expanded]
try:
return Millisatoshi(int(10**11 / float(json)))
except Exception:
plugin.log(level='info', message='{}: could not convert {} to msat'.format(url, json))
return None
def set_proxies(plugin):
config = plugin.rpc.listconfigs()
if 'always-use-proxy' in config and config['always-use-proxy']:
paddr = config['proxy']
# Default port in 9050
if ':' not in paddr:
paddr += ':9050'
plugin.proxies = {'https': 'socks5h://' + paddr,
'http': 'socks5h://' + paddr}
else:
plugin.proxies = None
# Don't grab these more than once per hour.
@cached(cache=TTLCache(maxsize=1024, ttl=3600))
def get_rates(plugin, currency):
rates = {}
for s in sources:
r = get_currencyrate(plugin, currency, s.urlformat, s.replymembers)
if r is not None:
rates[s.name] = r
return rates
@plugin.method("currencyrate")
def currencyrate(plugin, currency):
"""Gets currency from given APIs."""
return get_rates(plugin, currency.upper())
@plugin.method("currencyconvert")
def currencyconvert(plugin, amount, currency):
"""Converts currency using given APIs."""
rates = get_rates(plugin, currency.upper())
if len(rates) == 0:
raise Exception("No values available for currency {}".format(currency.upper()))
val = statistics.median([m.millisatoshis for m in rates.values()]) * float(amount)
return {"msat": Millisatoshi(round(val))}
@plugin.init()
def init(options, configuration, plugin):
set_proxies(plugin)
if options['add-source'] != '':
sourceopts = options['add-source']
# Prior to 0.9.3, 'multi' was unsupported.
if type(sourceopts) is not list:
sourceopts = [sourceopts]
for s in sourceopts:
parts = s.split(',')
sources.append(Source(parts[0], parts[1], parts[2:]))
if options['disable-source'] != '':
disableopts = options['disable-source']
# Prior to 0.9.3, 'multi' was unsupported.
if type(disableopts) is not list:
disableopts = [disableopts]
for s in sources[:]:
if s.name in disableopts:
sources.remove(s)
# As a bad example: binance,https://api.binance.com/api/v3/ticker/price?symbol=BTC{currency}T,price
plugin.add_option(name='add-source', default='', description='Add source name,urlformat,resultmembers...')
plugin.add_option(name='disable-source', default='', description='Disable source by name')
# This has an effect only for recent pyln versions (0.9.3+).
plugin.options['add-source']['multi'] = True
plugin.options['disable-source']['multi'] = True
plugin.run()