Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 57 additions & 53 deletions api/lightning/cln.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import hashlib
import os
import random
import secrets
import struct
import time
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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(
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions docker/cln/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "./entrypoint.sh" ]
6 changes: 3 additions & 3 deletions docker/cln/config
Original file line number Diff line number Diff line change
Expand Up @@ -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
# bookkeeper-db=postgres://user:pass@localhost:5433/cln
6 changes: 3 additions & 3 deletions docker/cln/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
fi
4 changes: 2 additions & 2 deletions scripts/generate_grpc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/
cp -r *_grpc.py /tmp/
8 changes: 4 additions & 4 deletions scripts/traditional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 7 additions & 7 deletions scripts/traditional/regtest-nodes
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$?"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down