diff --git a/api/lightning/cln.py b/api/lightning/cln.py index b005d4ece..12bcb88ce 100755 --- a/api/lightning/cln.py +++ b/api/lightning/cln.py @@ -1,6 +1,5 @@ import hashlib import os -import random import secrets import struct import time @@ -80,6 +79,17 @@ def get_info(cls): except Exception as e: print(f"Cannot get CLN node id: {e}") + @classmethod + def _get_hold_invoice(cls, payment_hash): + request = hold_pb2.ListRequest(payment_hash=bytes.fromhex(payment_hash)) + holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) + response = holdstub.List(request) + + if len(response.invoices) == 0: + return None + + return response.invoices[0] + @classmethod def newaddress(cls): """Only used on tests to fund the regtest node""" @@ -231,24 +241,20 @@ def pay_onchain(cls, onchainpayment, queue_code=5, on_mempool_code=2): @classmethod def cancel_return_hold_invoice(cls, payment_hash): """Cancels or returns a hold invoice""" - request = hold_pb2.HoldInvoiceCancelRequest( - payment_hash=bytes.fromhex(payment_hash) - ) + request = hold_pb2.CancelRequest(payment_hash=bytes.fromhex(payment_hash)) holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoiceCancel(request) + holdstub.Cancel(request) - return response.state == hold_pb2.Holdstate.CANCELED + return True @classmethod def settle_hold_invoice(cls, preimage): """settles a hold invoice""" - request = hold_pb2.HoldInvoiceSettleRequest( - payment_hash=hashlib.sha256(bytes.fromhex(preimage)).digest() - ) + request = hold_pb2.SettleRequest(payment_preimage=bytes.fromhex(preimage)) holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoiceSettle(request) + holdstub.Settle(request) - return response.state == hold_pb2.Holdstate.SETTLED + return True @classmethod def gen_hold_invoice( @@ -271,26 +277,25 @@ def gen_hold_invoice( # The preimage is a random hash of 256 bits entropy preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest() - request = hold_pb2.HoldInvoiceRequest( - description=description, - amount_msat=hold_pb2.Amount(msat=num_satoshis * 1_000), - label=f"Order:{order_id}-{lnpayment_concept}-{time}--{random.randint(1, 100000)}", + request = hold_pb2.InvoiceRequest( + payment_hash=hashlib.sha256(preimage).digest(), + memo=description, + amount_msat=num_satoshis * 1_000, expiry=invoice_expiry, - cltv=cltv_expiry_blocks, - preimage=preimage, # preimage is actually optional in cln, as cln would generate one by default + min_final_cltv_expiry=cltv_expiry_blocks, ) holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoice(request) + response = holdstub.Invoice(request) hold_payment["invoice"] = response.bolt11 payreq_decoded = cls.decode_payreq(hold_payment["invoice"]) hold_payment["preimage"] = preimage.hex() - hold_payment["payment_hash"] = response.payment_hash.hex() + hold_payment["payment_hash"] = payreq_decoded.payment_hash hold_payment["created_at"] = timezone.make_aware( datetime.fromtimestamp(payreq_decoded.created_at) ) hold_payment["expires_at"] = timezone.make_aware( - datetime.fromtimestamp(response.expires_at) + datetime.fromtimestamp(payreq_decoded.created_at + invoice_expiry) ) hold_payment["cltv_expiry"] = cltv_expiry_blocks @@ -301,23 +306,25 @@ def validate_hold_invoice_locked(cls, lnpayment): """Checks if hold invoice is locked""" from api.models import LNPayment - request = hold_pb2.HoldInvoiceLookupRequest( - payment_hash=bytes.fromhex(lnpayment.payment_hash) - ) - holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoiceLookup(request) + invoice = cls._get_hold_invoice(lnpayment.payment_hash) + if invoice is None: + return False # Will fail if 'empty result for listdatastore_state' or 'Invoice dropped from internal state unexpectedly'. Happens if invoice expiry # time has passed (but these are 15% padded at the moment). Should catch it # and report back that the invoice has expired (better robustness) - if response.state == hold_pb2.Holdstate.OPEN: + if invoice.state == hold_pb2.UNPAID: pass - if response.state == hold_pb2.Holdstate.SETTLED: + if invoice.state == hold_pb2.PAID: pass - if response.state == hold_pb2.Holdstate.CANCELED: + if invoice.state == hold_pb2.CANCELLED: pass - if response.state == hold_pb2.Holdstate.ACCEPTED: - lnpayment.expiry_height = response.htlc_expiry + if invoice.state == hold_pb2.ACCEPTED: + htlc_expiries = [ + htlc.cltv_expiry for htlc in invoice.htlcs if htlc.HasField("cltv_expiry") + ] + if len(htlc_expiries) > 0: + lnpayment.expiry_height = min(htlc_expiries) lnpayment.status = LNPayment.Status.LOCKED lnpayment.save(update_fields=["expiry_height", "status"]) return True @@ -334,33 +341,31 @@ def lookup_invoice_status(cls, lnpayment): expiry_height = 0 cln_response_state_to_lnpayment_status = { - 0: LNPayment.Status.INVGEN, # OPEN - 1: LNPayment.Status.SETLED, # SETTLED - 2: LNPayment.Status.CANCEL, # CANCELLED - 3: LNPayment.Status.LOCKED, # ACCEPTED + hold_pb2.UNPAID: LNPayment.Status.INVGEN, + hold_pb2.ACCEPTED: LNPayment.Status.LOCKED, + hold_pb2.PAID: LNPayment.Status.SETLED, + hold_pb2.CANCELLED: LNPayment.Status.CANCEL, } try: # this is similar to LNNnode.validate_hold_invoice_locked - request = hold_pb2.HoldInvoiceLookupRequest( - payment_hash=bytes.fromhex(lnpayment.payment_hash) - ) - holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoiceLookup(request) + invoice = cls._get_hold_invoice(lnpayment.payment_hash) + if invoice is None: + return status, expiry_height - status = cln_response_state_to_lnpayment_status[response.state] + status = cln_response_state_to_lnpayment_status[invoice.state] # try saving expiry height - if hasattr(response, "htlc_expiry"): - try: - expiry_height = response.htlc_expiry - except Exception: - pass + htlc_expiries = [ + htlc.cltv_expiry for htlc in invoice.htlcs if htlc.HasField("cltv_expiry") + ] + if len(htlc_expiries) > 0: + expiry_height = min(htlc_expiries) except Exception as e: - # If it fails at finding the invoice: it has been expired for more than an hour (and could be paid or just expired). + # If it fails at finding the invoice: it could have been paid, cancelled, or expired. # In RoboSats DB we make a distinction between cancelled and returned - # (holdinvoice plugin has separate state for hodl-invoices, which it forgets after an invoice expired more than an hour ago) + # (the hold plugin stores separate state for hold invoices) if "empty result for listdatastore_state" in str( e ) or "Invoice dropped from internal state unexpectedly" in str(e): @@ -862,16 +867,15 @@ def send_keysend( @classmethod def double_check_htlc_is_settled(cls, payment_hash): """Just as it sounds. Better safe than sorry!""" - request = hold_pb2.HoldInvoiceLookupRequest( - payment_hash=bytes.fromhex(payment_hash) - ) try: - holdstub = hold_pb2_grpc.HoldStub(cls.hold_channel) - response = holdstub.HoldInvoiceLookup(request) + invoice = cls._get_hold_invoice(payment_hash) except Exception as e: if "Timed out" in str(e): return False else: raise e - return response.state == hold_pb2.Holdstate.SETTLED + if invoice is None: + return False + + return invoice.state == hold_pb2.PAID diff --git a/docker/cln/Dockerfile b/docker/cln/Dockerfile index 2dba011bc..3d4d14f37 100644 --- a/docker/cln/Dockerfile +++ b/docker/cln/Dockerfile @@ -18,16 +18,16 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y RUN rustup toolchain install stable --component rustfmt --allow-downgrade WORKDIR /opt/lightningd -RUN git clone https://github.com/daywalker90/holdinvoice.git /tmp/holdinvoice -RUN cd /tmp/holdinvoice \ +RUN git clone https://github.com/BoltzExchange/hold.git /tmp/hold +RUN cd /tmp/hold \ && cargo build --release FROM elementsproject/lightningd:v24.08 as final -COPY --from=builder /tmp/holdinvoice/target/release/holdinvoice /tmp/holdinvoice +COPY --from=builder /tmp/hold/target/release/hold /tmp/hold COPY config /tmp/config COPY entrypoint.sh entrypoint.sh RUN chmod +x entrypoint.sh EXPOSE 9735 9835 -ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "./entrypoint.sh" ] \ No newline at end of file +ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "./entrypoint.sh" ] diff --git a/docker/cln/config b/docker/cln/config index 1210a9762..c83476758 100644 --- a/docker/cln/config +++ b/docker/cln/config @@ -3,8 +3,8 @@ proxy=127.0.0.1:9050 bind-addr=127.0.0.1:9736 addr=statictor:127.0.0.1:9051 grpc-port=9999 -grpc-hold-port=9998 +hold-grpc-port=9998 always-use-proxy=true -important-plugin=/root/.lightning/plugins/holdinvoice +important-plugin=/root/.lightning/plugins/hold # wallet=postgres://user:pass@localhost:5433/cln -# bookkeeper-db=postgres://user:pass@localhost:5433/cln \ No newline at end of file +# bookkeeper-db=postgres://user:pass@localhost:5433/cln diff --git a/docker/cln/entrypoint.sh b/docker/cln/entrypoint.sh index d9061091f..37956c8dd 100644 --- a/docker/cln/entrypoint.sh +++ b/docker/cln/entrypoint.sh @@ -17,11 +17,11 @@ if [ "$EXPOSE_TCP" == "true" ]; then socat "TCP4-listen:$LIGHTNINGD_RPC_PORT,fork,reuseaddr" "UNIX-CONNECT:${networkdatadir}/lightning-rpc" & fg %- else - # Always copy the holdinvoice plugin into the plugins directory on start up + # Always copy the hold plugin into the plugins directory on start up mkdir -p /root/.lightning/plugins - cp /tmp/holdinvoice /root/.lightning/plugins/holdinvoice + cp /tmp/hold /root/.lightning/plugins/hold if [ ! -f /root/.lightning/config ]; then cp /tmp/config /root/.lightning/config fi exec "$@" -fi \ No newline at end of file +fi diff --git a/scripts/generate_grpc.sh b/scripts/generate_grpc.sh index 3101b6616..c31022321 100755 --- a/scripts/generate_grpc.sh +++ b/scripts/generate_grpc.sh @@ -10,7 +10,7 @@ curl --parallel -o lightning.proto https://raw.githubusercontent.com/lightningne -o router.proto https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/routerrpc/router.proto \ -o signer.proto https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/signrpc/signer.proto \ -o verrpc.proto https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/verrpc/verrpc.proto \ - -o hold.proto https://raw.githubusercontent.com/daywalker90/holdinvoice/master/proto/hold.proto \ + -o hold.proto https://raw.githubusercontent.com/BoltzExchange/hold/main/protos/hold.proto \ -o primitives.proto https://raw.githubusercontent.com/ElementsProject/lightning/v24.08/cln-grpc/proto/primitives.proto \ -o node.proto https://raw.githubusercontent.com/ElementsProject/lightning/v24.08/cln-grpc/proto/node.proto @@ -43,4 +43,4 @@ echo "Done" # On development environments the local volume will be mounted over these files. We copy pb2 and grpc files to /tmp/. # This way, we can find if these files are missing with our entrypoint.sh and copy them into the volume. cp -r *_pb2.py /tmp/ -cp -r *_grpc.py /tmp/ \ No newline at end of file +cp -r *_grpc.py /tmp/ diff --git a/scripts/traditional/README.md b/scripts/traditional/README.md index f9e0a6fcb..09917c5f9 100644 --- a/scripts/traditional/README.md +++ b/scripts/traditional/README.md @@ -6,7 +6,7 @@ Binaries needed: * postgresql (`postgres`, `initdb`, `psql`) * redis (`redis-server`) * bitcoin (`bitcoind`, `bitcoin-cli`) -* cln (`lightningd`, `lightning-cli`, `holdinvoice`) +* cln (`lightningd`, `lightning-cli`, `hold`) * lnd (`lnd`, `lncli`) ## Preparation @@ -26,7 +26,7 @@ ln -sf /usr/lib/postgresql/16/bin/psql ~/.local/bin/ Bitcoin nodes if not already installed need to be manually downloaded. * bitcoin core binaries can be found here: https://bitcoincore.org/en/download * cln binaries can be found here: https://github.com/ElementsProject/lightning/releases -* holdinvoice binary can be found here: https://github.com/daywalker90/holdinvoice/releases +* hold binary can be built from here: https://github.com/BoltzExchange/hold * lnd binaries can be found here: https://github.com/lightningnetwork/lnd/releases Example preparation: @@ -42,7 +42,7 @@ $ cd traditional/programs # if you do not have them already installed $ mkdir bitcoin cln lnd -# download bitcoin, cln (and holdinvoice) and lnd binaries +# download bitcoin, cln (and hold) and lnd binaries # follow https://github.com/hoytech/strfry#compile $ git clone https://github.com/hoytech/strfry @@ -112,7 +112,7 @@ BITCOIND_BIN = "traditional/programs/bitcoin/bin/bitcoind" BITCOIN_CLI_BIN = "traditional/programs/bitcoin/bin/bitcoin-cli" LIGHTNINGD_BIN = "traditional/programs/cln/bin/lightningd" LIGHTNING_CLI_BIN = "traditional/programs/cln/bin/lightning-cli" -HOLDINVOICE_PLUGIN_BIN = "traditional/programs/cln/holdinvoice" +HOLD_PLUGIN_BIN = "traditional/programs/cln/hold" LND_BIN = "traditional/programs/lnd/lnd" LNCLI_BIN = "traditional/programs/lnd/lncli" STRFRY_GIT_DIR = "traditional/programs/strfry" diff --git a/scripts/traditional/regtest-nodes b/scripts/traditional/regtest-nodes index 478c017a2..114a554c5 100755 --- a/scripts/traditional/regtest-nodes +++ b/scripts/traditional/regtest-nodes @@ -121,7 +121,7 @@ _nodes_environment_set() { if [ "$cln_set" = true ]; then LIGHTNINGD_BIN="$(_get_env_var_path "LIGHTNINGD_BIN")" || return "$?" LIGHTNING_CLI_BIN="$(_get_env_var_path "LIGHTNING_CLI_BIN")" || return "$?" - HOLDINVOICE_PLUGIN_BIN="$(_get_env_var_path "HOLDINVOICE_PLUGIN_BIN")" || return "$?" + HOLD_PLUGIN_BIN="$(_get_env_var_path "HOLD_PLUGIN_BIN")" || return "$?" fi if [ "$lnd_set" = true ]; then LND_BIN="$(_get_env_var_path "LND_BIN")" || return "$?" @@ -182,12 +182,12 @@ _nodes_environment_set() { if ! _command_exist "$LIGHTNING_CLI_BIN"; then return 1 fi - if [ -z "$HOLDINVOICE_PLUGIN_BIN" ]; then - echo "error: $HOLDINVOICE_PLUGIN_BIN not set" >&2 + if [ -z "$HOLD_PLUGIN_BIN" ]; then + echo "error: HOLD_PLUGIN_BIN not set" >&2 return 1 fi - if [ ! -f "$HOLDINVOICE_PLUGIN_BIN" ]; then - echo "error: $HOLDINVOICE_PLUGIN_BIN plugin not found" >&2 + if [ ! -f "$HOLD_PLUGIN_BIN" ]; then + echo "error: $HOLD_PLUGIN_BIN plugin not found" >&2 return 1 fi fi @@ -477,8 +477,8 @@ database-upgrade=true log-file=lightning.log addr=localhost:$CLN_COORD_LISTEN_PORT grpc-port=$CLN_COORD_GRPC_PORT -important-plugin=$HOLDINVOICE_PLUGIN_BIN -grpc-hold-port=$CLN_COORD_HOLD_PORT +important-plugin=$HOLD_PLUGIN_BIN +hold-grpc-port=$CLN_COORD_HOLD_PORT disable-plugin=clnrest disable-plugin=wss-proxy EOF