SatSale

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 0831f51895d7291a4614e53c119b5dc80adb1c63
parent 2642243f9bfa4700e2d07744c0f162c534a37f27
Author: Nick <nicholas.w.farrow@gmail.com>
Date:   Fri, 12 Nov 2021 17:14:30 +1100

Add clightning support!! 👩‍💻 (#22)

* clightning implementation

* Add clightning config args

* formatting

* Correct node loading and invoice lookup in clightning

* Use proper unix domain sockets

* explain remote node tunnel in config

* add clightning to readme and docs

* Remove useless import

* uncomment bitcoind node rpc connection

* Fix missing host variable (not sure how this didnt show up earlier)
Diffstat:
MREADME.md | 5+++--
Mconfig.py | 20++++++++++++++++++++
Mdocs/lightning.md | 16+++++++++++++++-
Mgateways/ssh_tunnel.py | 35++++++++++++++++++++++++++++++++++-
Anode/clightning.py | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrequirements.txt | 1+
Msatsale.py | 14++++++++++++--
7 files changed, 172 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md @@ -30,8 +30,9 @@ SatSale currently serves as a 1. Donation page and button for your website that you can easily embed/link to anywhere. 2. Bitcoin payment gateway, including a Woocommerce plugin that easily turns any Wordpress site into a Bitcoin accepting store. 3. Versatile API and payments platform. +for both on-chain and lightning payments (supporting both clightning and lnd). -Other Bitcoin payment processors are known for being difficult to install and self-host. +Other Bitcoin payment processors are known for being difficult to install and self-host, SatSale is easy to modify, and build upon. SatSale makes donation buttons simple - easy copy paste the one line HTML iframe into your site. With a simple Python backend to talk to your own Bitcoin node, SatSale uses RPC to generate new addresses, and monitors the payment status with your own copy of the blockchain. @@ -64,7 +65,7 @@ username = "RPCUSERNAME" password = "RPCPASSWORD" ``` (You can find these in `~/.bitcoin/bitcoin.conf`). -When connecting to a remote node, also edit either the SSH `tunnel_host` (or see [tor hidden service](/docs/tor.md)). If you have a lightning node (lnd) and want to use lightning network payments, see [Lightning instructions](docs/lightning.md). More [example configs](docs/). +When connecting to a remote node, also edit either the SSH `tunnel_host` (or see [tor hidden service](/docs/tor.md)). If you have a lightning node (lnd or clightning) and want to use lightning network payments, see [Lightning instructions](docs/lightning.md). More [example configs](docs/). ### Run SatSale Run SatSale with diff --git a/config.py b/config.py @@ -60,10 +60,30 @@ connection_attempts = 3 # Generic redirect url after payment redirect = "https://github.com/nickfarrow/satsale" +#### Payment Nodes #### +pay_method = "bitcoind" + +# Switch payment_method to lnd if you want to use lightning payments instead. And uncomment lnd_dir. +#pay_method = "lnd" +# lnd_dir is only needed if you want to copy macaroon and TLS cert locally +#lnd_dir = "~/.lnd/" +#lnd_rpcport = "10009" +#lnd_macaroon = "invoice.macaroon" +#lnd_cert = "tls.cert" + +# Or clightning +#pay_method = "clightning" + +# If remote clightning, make sure `ssh -nNT -L lightning-rpc:{clightning_rpc_file} {tunnel_host}` +# creates a lightning-rpc unix domain socket +#clightning_rpc_file = "/home/user/.lightning/lightning-rpc" +####################### + # Lightning Address e.g. name@you.satsale.domain (think this requires https url) lightning_address = None lightning_address_comment = None # Defaults to: "Thank you for your support <3" + # DO NOT CHANGE THIS TO TRUE UNLESS YOU WANT ALL PAYMENTS TO AUTOMATICALLY # BE CONSIDERED AS PAID. free_mode = False diff --git a/docs/lightning.md b/docs/lightning.md @@ -1,8 +1,11 @@ # Lightning Support -Recently we have added support for Lightning Network Daemon, with plans to extend support to clightning in the near future. An example config can be found in [/docs/config_lightning.py](/docs/config_lightning.py). +We support both Lightning Network Daemon (lnd) and clightning. +An example config for lnd can be found in [/docs/config_lightning.py](/docs/config_lightning.py). If installing the python library lndgrpc requirement failed, see this [solution](https://stackoverflow.com/questions/56357794/unable-to-install-grpcio-using-pip-install-grpcio#comment113013007_62500932). + +## LND To use lightning, you need to change your `pay_method` in `config.py`, and set your lightning directory on your node. ```python pay_method = "lnd" @@ -10,6 +13,17 @@ lnd_dir = "~/.lnd/" lnd_rpcport = "10009" ``` + +## clightning +To use lightning, you need to change your `pay_method` in `config.py`, and set your lightning directory on your node. +```python +pay_method = "clightning" +# If remote clightning, make sure `ssh -nNT -L lightning-rpc:{clightning_rpc_file} {tunnel_host}` +clightning_rpc_file = "/home/user/.lightning/lightning-rpc" +``` + + +## Notes Your lnd directory is used to find your `.tls` and `.macaroon` files that are required to talk to your lightning node. They are copied over SSH into your SatSale folder. If this copy fails, perhaps copy them manually and they will be identified on start up. Your node will require sufficient liquidity and connection to receive payments. diff --git a/gateways/ssh_tunnel.py b/gateways/ssh_tunnel.py @@ -1,5 +1,6 @@ import subprocess import time +import os import config from node import bitcoind @@ -8,7 +9,7 @@ from node import bitcoind def open_tunnel(host, port): # If tunnel is required (might make things easier) try: - if host is not None: + if config.tunnel_host is not None: command = [ "ssh", config.tunnel_host, @@ -29,6 +30,34 @@ def open_tunnel(host, port): pass return +def clightning_unix_domain_socket_ssh(rpc_store_dir=None): + if rpc_store_dir is None: + rpc_store_dir = os.getcwd() + + # ssh -nNT -L lightning-rpc:~/.lightning/lightning-rpc config.tunnel_host + if config.tunnel_host is not None: + try: + command = [ + "ssh", + "-nNT", + "-L", + "lightning-rpc:{}".format(config.clightning_rpc_file), + "{}".format(config.tunnel_host), + ] + print("Opening tunnel to {}.".format(" ".join(command))) + tunnel_proc = subprocess.Popen(command) + return tunnel_proc + + except Exception as e: + print("FAILED TO OPEN UNIX DOMAIN SOCKET OVER SSH. Exception: {}".format(e)) + tunnel_proc = None + pass + + else: + tunnel_proc = None + + return + def close_tunnel(): if tunnel_proc is not None: @@ -45,6 +74,10 @@ if config.tunnel_host is not None: if "lnd_rpcport" in config.__dict__.keys(): open_tunnel(config.tunnel_host, config.lnd_rpcport) + # And if clightning is enabled + if "clightning_rpc_file" in config.__dict__.keys(): + clightning_unix_domain_socket_ssh() + time.sleep(2) else: tunnel_proc = None diff --git a/node/clightning.py b/node/clightning.py @@ -0,0 +1,87 @@ +import subprocess +import pathlib +import time +import os +import json +import uuid +import qrcode + + +from payments.price_feed import get_btc_value +import config + +# if False: # config.tor_clightningrpc_host is not None: +# from gateways.tor import session +# else: +# import requests +# +# session = None + +class clightning: + def __init__(self): + from pyln.client import LightningRpc + + for i in range(config.connection_attempts): + try: + print("Attempting to connect to clightning...") + self.clightning = LightningRpc(config.clightning_rpc_file) + + print("Getting clightning info...") + info = self.clightning.getinfo() + print(info) + + print("Successfully clightning lnd.") + break + + except Exception as e: + print(e) + time.sleep(config.pollrate) + print( + "Attempting again... {}/{}...".format( + i + 1, config.connection_attempts + ) + ) + else: + raise Exception( + "Could not connect to clightning. Check your port tunneling settings and try again." + ) + + print("Ready for payments requests.") + return + + def create_qr(self, uuid, address, value): + qr_str = "{}".format(address.upper()) + img = qrcode.make(qr_str) + img.save("static/qr_codes/{}.png".format(uuid)) + return + + # Create lightning invoice + def create_clightning_invoice(self, btc_amount, label): + # Multiplying by 10^8 to convert to satoshi units + msats_amount = int(btc_amount * 10 ** (3+8)) + lnd_invoice = self.clightning.invoice(msats_amount, label, "SatSale-{}".format(label)) + return lnd_invoice["bolt11"], lnd_invoice["payment_hash"] + + def get_address(self, amount, label): + address, r_hash = self.create_clightning_invoice(amount, label) + return address, r_hash + + # Check whether the payment has been paid + def check_payment(self, uuid): + invoices = self.clightning.listinvoices(uuid)['invoices'] + + if len(invoices) == 0: + print("Could not find invoice on node. Something's wrong.") + return 0, 0 + + invoice = invoices[0] + + if invoice["status"] != "paid": + conf_paid = 0 + unconf_paid = 0 + else: + # Store amount paid and convert to BTC units + conf_paid = int(invoice["msatoshi_received"]) / 10**(3+8) + unconf_paid = 0 + + return conf_paid, unconf_paid diff --git a/requirements.txt b/requirements.txt @@ -14,3 +14,4 @@ PySocks==1.7.1 # For lightning (optional) setuptools==50.3.2 lnd-grpc-client +pyln-client diff --git a/satsale.py b/satsale.py @@ -21,6 +21,8 @@ from payments import database from payments.price_feed import get_btc_value from node import bitcoind from node import lnd +from node import clightning + from gateways import woo_webhook app = Flask(__name__) @@ -246,6 +248,9 @@ def check_payment_status(uuid): node = get_node(invoice["method"]) if invoice["method"] == "lnd": conf_paid, unconf_paid = node.check_payment(invoice["rhash"]) + elif invoice["method"] == "clightning": + # Lookup clightning invoice based on label (uuid) + conf_paid, unconf_paid = node.check_payment(invoice["uuid"]) else: conf_paid, unconf_paid = node.check_payment(invoice["address"]) @@ -253,7 +258,7 @@ def check_payment_status(uuid): dbg_free_mode_cond = config.free_mode and (time.time() - invoice["time"] > 5) # If payment is paid - if (conf_paid > invoice["btc_value"]) or dbg_free_mode_cond: + if (conf_paid >= invoice["btc_value"]) or dbg_free_mode_cond: status.update( { "payment_complete": 1, @@ -279,6 +284,8 @@ def get_node(payment_method): node = bitcoin_node elif payment_method == "lnd": node = lightning_node + elif payment_method == "clightning": + node = lightning_node else: node = None return node @@ -296,7 +303,10 @@ bitcoin_node = bitcoind.btcd() print("Connection to bitcoin node successful.") if config.pay_method == "lnd": lightning_node = lnd.lnd() - print("Connection to lightning node successful.") + print("Connection to lightning node (lnd) successful.") +elif config.pay_method == "clightning": + lightning_node = clightning.clightning() + print("Connection to lightning node (clightning) successful.") if config.lightning_address is not None: