2023-05-22 14:56:15 +00:00
import hashlib
import os
import secrets
import struct
import time
from datetime import datetime , timedelta
import grpc
import ring
from decouple import config
from django . utils import timezone
2023-11-08 14:25:34 +00:00
from . import hold_pb2 , hold_pb2_grpc , node_pb2 , node_pb2_grpc
2023-05-22 14:56:15 +00:00
from . import primitives_pb2 as primitives__pb2
#######
# Works with CLN
#######
# Load the client's certificate and key
2023-05-25 11:19:45 +00:00
CLN_DIR = config ( " CLN_DIR " , cast = str , default = " /cln/testnet/ " )
with open ( os . path . join ( CLN_DIR , " client.pem " ) , " rb " ) as f :
2023-05-22 14:56:15 +00:00
client_cert = f . read ( )
2023-05-25 11:19:45 +00:00
with open ( os . path . join ( CLN_DIR , " client-key.pem " ) , " rb " ) as f :
2023-05-22 14:56:15 +00:00
client_key = f . read ( )
# Load the server's certificate
2023-05-25 11:19:45 +00:00
with open ( os . path . join ( CLN_DIR , " server.pem " ) , " rb " ) as f :
2023-05-22 14:56:15 +00:00
server_cert = f . read ( )
2023-05-25 11:19:45 +00:00
CLN_GRPC_HOST = config ( " CLN_GRPC_HOST " , cast = str , default = " localhost:9999 " )
2023-09-06 15:18:22 +00:00
CLN_GRPC_HOLD_HOST = config ( " CLN_GRPC_HOLD_HOST " , cast = str , default = " localhost:9998 " )
2023-05-22 14:56:15 +00:00
DISABLE_ONCHAIN = config ( " DISABLE_ONCHAIN " , cast = bool , default = True )
MAX_SWAP_AMOUNT = config ( " MAX_SWAP_AMOUNT " , cast = int , default = 500000 )
class CLNNode :
os . environ [ " GRPC_SSL_CIPHER_SUITES " ] = " HIGH+ECDSA "
# Create the SSL credentials object
creds = grpc . ssl_channel_credentials (
root_certificates = server_cert ,
private_key = client_key ,
certificate_chain = client_cert ,
)
# Create the gRPC channel using the SSL credentials
2023-09-06 15:18:22 +00:00
hold_channel = grpc . secure_channel ( CLN_GRPC_HOLD_HOST , creds )
node_channel = grpc . secure_channel ( CLN_GRPC_HOST , creds )
2023-05-22 14:56:15 +00:00
payment_failure_context = {
- 1 : " Catchall nonspecific error. " ,
201 : " Already paid with this hash using different amount or destination. " ,
203 : " Permanent failure at destination. " ,
205 : " Unable to find a route. " ,
206 : " Route too expensive. " ,
207 : " Invoice expired. " ,
210 : " Payment timed out without a payment in progress. " ,
}
@classmethod
def get_version ( cls ) :
try :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
2023-11-08 14:25:34 +00:00
request = node_pb2 . GetinfoRequest ( )
2023-11-08 14:56:36 +00:00
response = nodestub . Getinfo ( request )
2023-05-22 14:56:15 +00:00
return response . version
except Exception as e :
2023-11-08 14:25:34 +00:00
print ( f " Cannot get CLN version: { e } " )
2023-11-14 01:42:04 +00:00
return " Not installed "
2023-05-22 14:56:15 +00:00
2023-11-13 14:40:47 +00:00
@classmethod
def get_info ( cls ) :
try :
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
request = node_pb2 . GetinfoRequest ( )
response = nodestub . Getinfo ( request )
return response
except Exception as e :
print ( f " Cannot get CLN node id: { e } " )
2023-11-15 19:48:04 +00:00
@classmethod
def newaddress ( cls ) :
""" Only used on tests to fund the regtest node """
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
request = node_pb2 . NewaddrRequest ( )
response = nodestub . NewAddr ( request )
return response . bech32
2023-05-22 14:56:15 +00:00
@classmethod
def decode_payreq ( cls , invoice ) :
""" Decodes a lightning payment request (invoice) """
2024-06-17 21:33:49 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
request = node_pb2 . DecodeRequest ( string = invoice )
response = nodestub . Decode ( request )
2023-05-22 14:56:15 +00:00
return response
@classmethod
def estimate_fee ( cls , amount_sats , target_conf = 2 , min_confs = 1 ) :
""" Returns estimated fee for onchain payouts """
# feerate estimaes work a bit differently in cln see https://lightning.readthedocs.io/lightning-feerates.7.html
2023-11-08 14:25:34 +00:00
request = node_pb2 . FeeratesRequest ( style = " PERKB " )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . Feerates ( request )
2023-05-22 14:56:15 +00:00
# "opening" -> ~12 block target
return {
" mining_fee_sats " : response . onchain_fee_estimates . opening_channel_satoshis ,
" mining_fee_rate " : response . perkb . opening / 1000 ,
}
wallet_balance_cache = { }
@ring.dict ( wallet_balance_cache , expire = 10 ) # keeps in cache for 10 seconds
@classmethod
def wallet_balance ( cls ) :
""" Returns onchain balance """
2023-11-08 14:25:34 +00:00
request = node_pb2 . ListfundsRequest ( )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . ListFunds ( request )
2023-05-22 14:56:15 +00:00
unconfirmed_balance = 0
confirmed_balance = 0
total_balance = 0
for utxo in response . outputs :
if not utxo . reserved :
if (
utxo . status
2023-11-08 14:25:34 +00:00
== node_pb2 . ListfundsOutputs . ListfundsOutputsStatus . UNCONFIRMED
2023-05-22 14:56:15 +00:00
) :
unconfirmed_balance + = utxo . amount_msat . msat / / 1_000
total_balance + = utxo . amount_msat . msat / / 1_000
elif (
utxo . status
2023-11-08 14:25:34 +00:00
== node_pb2 . ListfundsOutputs . ListfundsOutputsStatus . CONFIRMED
2023-05-22 14:56:15 +00:00
) :
confirmed_balance + = utxo . amount_msat . msat / / 1_000
total_balance + = utxo . amount_msat . msat / / 1_000
return {
" total_balance " : total_balance ,
" confirmed_balance " : confirmed_balance ,
" unconfirmed_balance " : unconfirmed_balance ,
}
channel_balance_cache = { }
@ring.dict ( channel_balance_cache , expire = 10 ) # keeps in cache for 10 seconds
@classmethod
def channel_balance ( cls ) :
""" Returns channels balance """
2023-11-08 14:25:34 +00:00
request = node_pb2 . ListpeerchannelsRequest ( )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . ListPeerChannels ( request )
2023-05-22 14:56:15 +00:00
local_balance_sat = 0
remote_balance_sat = 0
unsettled_local_balance = 0
unsettled_remote_balance = 0
for channel in response . channels :
if (
channel . state
2023-11-08 14:25:34 +00:00
== node_pb2 . ListpeerchannelsChannels . ListpeerchannelsChannelsState . CHANNELD_NORMAL
2023-05-22 14:56:15 +00:00
) :
local_balance_sat + = channel . to_us_msat . msat / / 1_000
remote_balance_sat + = (
channel . total_msat . msat - channel . to_us_msat . msat
) / / 1_000
for htlc in channel . htlcs :
if (
htlc . direction
2023-11-08 14:25:34 +00:00
== node_pb2 . ListpeerchannelsChannelsHtlcs . ListpeerchannelsChannelsHtlcsDirection . IN
2023-05-22 14:56:15 +00:00
) :
unsettled_local_balance + = htlc . amount_msat . msat / / 1_000
elif (
htlc . direction
2023-11-08 14:25:34 +00:00
== node_pb2 . ListpeerchannelsChannelsHtlcs . ListpeerchannelsChannelsHtlcsDirection . OUT
2023-05-22 14:56:15 +00:00
) :
unsettled_remote_balance + = htlc . amount_msat . msat / / 1_000
return {
" local_balance " : local_balance_sat ,
" remote_balance " : remote_balance_sat ,
" unsettled_local_balance " : unsettled_local_balance ,
" unsettled_remote_balance " : unsettled_remote_balance ,
}
@classmethod
def pay_onchain ( cls , onchainpayment , queue_code = 5 , on_mempool_code = 2 ) :
""" Send onchain transaction for buyer payouts """
if DISABLE_ONCHAIN or onchainpayment . sent_satoshis > MAX_SWAP_AMOUNT :
return False
2023-11-08 14:25:34 +00:00
request = node_pb2 . WithdrawRequest (
2023-05-22 14:56:15 +00:00
destination = onchainpayment . address ,
satoshi = primitives__pb2 . AmountOrAll (
amount = primitives__pb2 . Amount ( msat = onchainpayment . sent_satoshis * 1_000 )
) ,
feerate = primitives__pb2 . Feerate (
perkb = int ( onchainpayment . mining_fee_rate ) * 1_000
) ,
minconf = int ( not config ( " SPEND_UNCONFIRMED " , default = False , cast = bool ) ) ,
)
# Cheap security measure to ensure there has been some non-deterministic time between request and DB check
delay = (
secrets . randbelow ( 2 * * 256 ) / ( 2 * * 256 ) * 10
) # Random uniform 0 to 5 secs with good entropy
2023-11-23 17:52:57 +00:00
if not config ( " TESTING " , cast = bool , default = False ) :
time . sleep ( 3 + delay )
2023-05-22 14:56:15 +00:00
if onchainpayment . status == queue_code :
# Changing the state to "MEMPO" should be atomic with SendCoins.
onchainpayment . status = on_mempool_code
onchainpayment . save ( update_fields = [ " status " ] )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . Withdraw ( request )
2023-05-22 14:56:15 +00:00
if response . txid :
onchainpayment . txid = response . txid . hex ( )
onchainpayment . broadcasted = True
onchainpayment . save ( update_fields = [ " txid " , " broadcasted " ] )
return True
elif onchainpayment . status == on_mempool_code :
# Bug, double payment attempted
return True
@classmethod
def cancel_return_hold_invoice ( cls , payment_hash ) :
""" Cancels or returns a hold invoice """
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceCancelRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( payment_hash )
)
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoiceCancel ( request )
2023-05-22 14:56:15 +00:00
2024-06-17 21:33:49 +00:00
return response . state == hold_pb2 . Holdstate . CANCELED
2023-05-22 14:56:15 +00:00
@classmethod
def settle_hold_invoice ( cls , preimage ) :
""" settles a hold invoice """
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceSettleRequest (
2023-05-22 14:56:15 +00:00
payment_hash = hashlib . sha256 ( bytes . fromhex ( preimage ) ) . digest ( )
)
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoiceSettle ( request )
2023-05-22 14:56:15 +00:00
2024-06-17 21:33:49 +00:00
return response . state == hold_pb2 . Holdstate . SETTLED
2023-05-22 14:56:15 +00:00
@classmethod
def gen_hold_invoice (
cls ,
num_satoshis ,
description ,
invoice_expiry ,
cltv_expiry_blocks ,
order_id ,
lnpayment_concept ,
time ,
) :
""" Generates hold invoice """
# constant 100h invoice expiry because cln has to cancel htlcs if invoice expires
# or it can't associate them anymore
invoice_expiry = cltv_expiry_blocks * 10 * 60
hold_payment = { }
# The preimage is a random hash of 256 bits entropy
preimage = hashlib . sha256 ( secrets . token_bytes ( nbytes = 32 ) ) . digest ( )
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceRequest (
2023-05-22 14:56:15 +00:00
description = description ,
2024-06-17 21:33:49 +00:00
amount_msat = hold_pb2 . Amount ( msat = num_satoshis * 1_000 ) ,
2023-05-22 14:56:15 +00:00
label = f " Order: { order_id } - { lnpayment_concept } - { time } " ,
expiry = invoice_expiry ,
cltv = cltv_expiry_blocks ,
preimage = preimage , # preimage is actually optional in cln, as cln would generate one by default
)
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoice ( request )
2023-05-22 14:56:15 +00:00
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 [ " created_at " ] = timezone . make_aware (
2024-06-17 21:33:49 +00:00
datetime . fromtimestamp ( payreq_decoded . created_at )
2023-05-22 14:56:15 +00:00
)
hold_payment [ " expires_at " ] = timezone . make_aware (
datetime . fromtimestamp ( response . expires_at )
)
hold_payment [ " cltv_expiry " ] = cltv_expiry_blocks
return hold_payment
@classmethod
def validate_hold_invoice_locked ( cls , lnpayment ) :
""" Checks if hold invoice is locked """
from api . models import LNPayment
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceLookupRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( lnpayment . payment_hash )
)
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoiceLookup ( request )
2023-05-22 14:56:15 +00:00
# Will fail if 'unable to locate invoice'. 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)
2024-06-17 21:33:49 +00:00
if response . state == hold_pb2 . Holdstate . OPEN :
2023-05-22 14:56:15 +00:00
pass
2024-06-17 21:33:49 +00:00
if response . state == hold_pb2 . Holdstate . SETTLED :
2023-05-22 14:56:15 +00:00
pass
2024-06-17 21:33:49 +00:00
if response . state == hold_pb2 . Holdstate . CANCELED :
2023-05-22 14:56:15 +00:00
pass
2024-06-17 21:33:49 +00:00
if response . state == hold_pb2 . Holdstate . ACCEPTED :
2023-05-22 14:56:15 +00:00
lnpayment . expiry_height = response . htlc_expiry
lnpayment . status = LNPayment . Status . LOCKED
lnpayment . save ( update_fields = [ " expiry_height " , " status " ] )
return True
@classmethod
def lookup_invoice_status ( cls , lnpayment ) :
"""
Returns the status ( as LNpayment . Status ) of the given payment_hash
If unchanged , returns the previous status
"""
from api . models import LNPayment
status = lnpayment . status
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
}
try :
# this is similar to LNNnode.validate_hold_invoice_locked
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceLookupRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( lnpayment . payment_hash )
)
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoiceLookup ( request )
2023-05-22 14:56:15 +00:00
status = cln_response_state_to_lnpayment_status [ response . state ]
# try saving expiry height
if hasattr ( response , " htlc_expiry " ) :
try :
expiry_height = response . htlc_expiry
except Exception :
pass
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).
# In RoboSats DB we make a distinction between cancelled and returned
2024-06-17 21:33:49 +00:00
# (holdinvoice plugin has separate state for hodl-invoices, which it forgets after an invoice expired more than an hour ago)
2023-05-22 14:56:15 +00:00
if " empty result for listdatastore_state " in str ( e ) :
print ( str ( e ) )
2023-11-08 14:25:34 +00:00
request2 = node_pb2 . ListinvoicesRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( lnpayment . payment_hash )
)
try :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response2 = nodestub . ListInvoices ( request2 ) . invoices
2023-05-22 14:56:15 +00:00
except Exception as e :
print ( str ( e ) )
if (
response2 [ 0 ] . status
2023-11-08 14:25:34 +00:00
== node_pb2 . ListinvoicesInvoices . ListinvoicesInvoicesStatus . PAID
2023-05-22 14:56:15 +00:00
) :
status = LNPayment . Status . SETLED
elif (
response2 [ 0 ] . status
2023-11-08 14:25:34 +00:00
== node_pb2 . ListinvoicesInvoices . ListinvoicesInvoicesStatus . EXPIRED
2023-05-22 14:56:15 +00:00
) :
status = LNPayment . Status . CANCEL
else :
print ( str ( e ) )
# Other write to logs
else :
print ( str ( e ) )
return status , expiry_height
@classmethod
def resetmc ( cls ) :
# don't think an equivalent exists for cln, maybe deleting gossip_store file?
return False
@classmethod
def validate_ln_invoice ( cls , invoice , num_satoshis , routing_budget_ppm ) :
""" Checks if the submited LN invoice comforms to expectations """
payout = {
" valid " : False ,
" context " : None ,
" description " : None ,
" payment_hash " : None ,
" created_at " : None ,
" expires_at " : None ,
}
try :
payreq_decoded = cls . decode_payreq ( invoice )
except Exception :
payout [ " context " ] = {
" bad_invoice " : " Does not look like a valid lightning invoice "
}
return payout
# Some wallet providers (e.g. Muun) force routing through a private channel with high fees >1500ppm
# These payments will fail. So it is best to let the user know in advance this invoice is not valid.
2024-06-17 21:33:49 +00:00
route_hints = payreq_decoded . routes . hints
2023-05-22 14:56:15 +00:00
# Max amount RoboSats will pay for routing
if routing_budget_ppm == 0 :
max_routing_fee_sats = max (
num_satoshis * float ( config ( " PROPORTIONAL_ROUTING_FEE_LIMIT " ) ) ,
float ( config ( " MIN_FLAT_ROUTING_FEE_LIMIT_REWARD " ) ) ,
)
else :
max_routing_fee_sats = int (
float ( num_satoshis ) * float ( routing_budget_ppm ) / 1000000
)
if route_hints :
routes_cost = [ ]
# For every hinted route...
for hinted_route in route_hints :
route_cost = 0
# ...add up the cost of every hinted hop...
for hop_hint in hinted_route . hops :
2024-06-17 21:33:49 +00:00
route_cost + = hop_hint . fee_base_msat . msat / 1_000
route_cost + = (
hop_hint . fee_proportional_millionths * num_satoshis / 1_000_000
)
2023-05-22 14:56:15 +00:00
# ...and store the cost of the route to the array
routes_cost . append ( route_cost )
# If the cheapest possible private route is more expensive than what RoboSats is willing to pay
if min ( routes_cost ) > = max_routing_fee_sats :
payout [ " context " ] = {
2024-09-03 13:32:22 +00:00
" bad_invoice " : " The invoice hinted private routes are not payable within the submitted routing budget. This can be adjusted with Advanced Options enabled. "
2023-05-22 14:56:15 +00:00
}
return payout
if payreq_decoded . amount_msat . msat == 0 :
payout [ " context " ] = {
" bad_invoice " : " The invoice provided has no explicit amount "
}
return payout
if not payreq_decoded . amount_msat . msat / / 1_000 == num_satoshis :
payout [ " context " ] = {
" bad_invoice " : " The invoice provided is not for "
+ " {:,} " . format ( num_satoshis )
+ " Sats "
}
return payout
payout [ " created_at " ] = timezone . make_aware (
2024-06-17 21:33:49 +00:00
datetime . fromtimestamp ( payreq_decoded . created_at )
2023-05-22 14:56:15 +00:00
)
payout [ " expires_at " ] = payout [ " created_at " ] + timedelta (
seconds = payreq_decoded . expiry
)
if payout [ " expires_at " ] < timezone . now ( ) :
payout [ " context " ] = {
" bad_invoice " : " The invoice provided has already expired "
}
return payout
payout [ " valid " ] = True
payout [ " description " ] = payreq_decoded . description
payout [ " payment_hash " ] = payreq_decoded . payment_hash . hex ( )
return payout
@classmethod
def pay_invoice ( cls , lnpayment ) :
""" Sends sats. Used for rewards payouts """
from api . models import LNPayment
fee_limit_sat = int (
max (
lnpayment . num_satoshis
* float ( config ( " PROPORTIONAL_ROUTING_FEE_LIMIT " ) ) ,
float ( config ( " MIN_FLAT_ROUTING_FEE_LIMIT_REWARD " ) ) ,
)
) # 200 ppm or 10 sats
timeout_seconds = int ( config ( " REWARDS_TIMEOUT_SECONDS " ) )
2023-11-08 14:25:34 +00:00
request = node_pb2 . PayRequest (
2023-05-22 14:56:15 +00:00
bolt11 = lnpayment . invoice ,
maxfee = primitives__pb2 . Amount ( msat = fee_limit_sat * 1_000 ) ,
retry_for = timeout_seconds ,
)
try :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . Pay ( request )
2023-05-22 14:56:15 +00:00
2023-11-08 14:25:34 +00:00
if response . status == node_pb2 . PayResponse . PayStatus . COMPLETE :
2023-05-22 14:56:15 +00:00
lnpayment . status = LNPayment . Status . SUCCED
lnpayment . fee = (
float ( response . amount_sent_msat . msat - response . amount_msat . msat )
/ 1000
)
lnpayment . preimage = response . payment_preimage . hex ( )
lnpayment . save ( update_fields = [ " fee " , " status " , " preimage " ] )
return True , None
2023-11-08 14:25:34 +00:00
elif response . status == node_pb2 . PayResponse . PayStatus . PENDING :
2023-05-22 14:56:15 +00:00
failure_reason = " Payment isn ' t failed (yet) "
lnpayment . failure_reason = LNPayment . FailureReason . NOTYETF
lnpayment . status = LNPayment . Status . FLIGHT
lnpayment . save ( update_fields = [ " failure_reason " , " status " ] )
return False , failure_reason
2023-11-08 14:25:34 +00:00
else : # response.status == node_pb2.PayResponse.PayStatus.FAILED
2023-05-22 14:56:15 +00:00
failure_reason = " All possible routes were tried and failed permanently. Or were no routes to the destination at all. "
lnpayment . failure_reason = LNPayment . FailureReason . NOROUTE
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . save ( update_fields = [ " failure_reason " , " status " ] )
return False , failure_reason
except grpc . _channel . _InactiveRpcError as e :
status_code = int ( e . details ( ) . split ( " code: Some( " ) [ 1 ] . split ( " ) " ) [ 0 ] )
failure_reason = cls . payment_failure_context [ status_code ]
lnpayment . failure_reason = LNPayment . FailureReason . NOROUTE
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . save ( update_fields = [ " failure_reason " , " status " ] )
return False , failure_reason
@classmethod
def follow_send_payment ( cls , lnpayment , fee_limit_sat , timeout_seconds ) :
""" Sends sats to buyer, continuous update """
from api . models import LNPayment , Order
hash = lnpayment . payment_hash
# retry_for is not quite the same as a timeout. Pay can still take SIGNIFICANTLY longer to return if htlcs are stuck!
# allow_self_payment=True, No such thing in pay command and self_payments do not work with pay!
2023-11-08 14:25:34 +00:00
request = node_pb2 . PayRequest (
2023-05-22 14:56:15 +00:00
bolt11 = lnpayment . invoice ,
maxfee = primitives__pb2 . Amount ( msat = fee_limit_sat * 1_000 ) ,
retry_for = timeout_seconds ,
)
order = lnpayment . order_paid_LN
if order . trade_escrow . num_satoshis < lnpayment . num_satoshis :
print ( f " Order: { order . id } Payout is larger than collateral !? " )
return
def watchpayment ( ) :
2023-11-08 14:25:34 +00:00
request_listpays = node_pb2 . ListpaysRequest (
payment_hash = bytes . fromhex ( hash )
)
2023-05-22 14:56:15 +00:00
while True :
try :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response_listpays = nodestub . ListPays ( request_listpays )
2023-05-22 14:56:15 +00:00
except Exception as e :
print ( str ( e ) )
time . sleep ( 2 )
continue
if (
len ( response_listpays . pays ) == 0
or response_listpays . pays [ 0 ] . status
2023-11-08 14:25:34 +00:00
!= node_pb2 . ListpaysPays . ListpaysPaysStatus . PENDING
2023-05-22 14:56:15 +00:00
) :
return response_listpays
else :
time . sleep ( 2 )
def handle_response ( ) :
try :
lnpayment . status = LNPayment . Status . FLIGHT
lnpayment . in_flight = True
lnpayment . save ( update_fields = [ " in_flight " , " status " ] )
2023-08-06 17:48:20 +00:00
order . update_status ( Order . Status . PAY )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . Pay ( request )
2023-05-22 14:56:15 +00:00
2023-11-08 14:25:34 +00:00
if response . status == node_pb2 . PayResponse . PayStatus . PENDING :
2023-05-22 14:56:15 +00:00
print ( f " Order: { order . id } IN_FLIGHT. Hash { hash } " )
watchpayment ( )
handle_response ( )
2023-11-08 14:25:34 +00:00
if response . status == node_pb2 . PayResponse . PayStatus . FAILED :
2023-05-22 14:56:15 +00:00
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . last_routing_time = timezone . now ( )
lnpayment . routing_attempts + = 1
lnpayment . failure_reason = LNPayment . FailureReason . NOROUTE
lnpayment . in_flight = False
if lnpayment . routing_attempts > 2 :
lnpayment . status = LNPayment . Status . EXPIRE
lnpayment . routing_attempts = 0
lnpayment . save (
update_fields = [
" status " ,
" last_routing_time " ,
" routing_attempts " ,
" failure_reason " ,
" in_flight " ,
]
)
order . expires_at = timezone . now ( ) + timedelta (
seconds = order . t_to_expire ( Order . Status . FAI )
)
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . FAI )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
2023-05-22 14:56:15 +00:00
print (
f " Order: { order . id } FAILED. Hash: { hash } Reason: { cls . payment_failure_context [ - 1 ] } "
)
2023-08-06 17:48:20 +00:00
order . log (
f " Payment LNPayment( { lnpayment . payment_hash } , { str ( lnpayment ) } ) failed. Failure reason: { cls . payment_failure_context [ - 1 ] } ) "
)
2023-05-22 14:56:15 +00:00
return {
" succeded " : False ,
" context " : f " payment failure reason: { cls . payment_failure_context [ - 1 ] } " ,
}
2023-11-08 14:25:34 +00:00
if response . status == node_pb2 . PayResponse . PayStatus . COMPLETE :
2023-05-22 14:56:15 +00:00
print ( f " Order: { order . id } SUCCEEDED. Hash: { hash } " )
lnpayment . status = LNPayment . Status . SUCCED
lnpayment . fee = (
float (
response . amount_sent_msat . msat - response . amount_msat . msat
)
/ 1000
)
lnpayment . preimage = response . payment_preimage . hex ( )
lnpayment . save ( update_fields = [ " status " , " fee " , " preimage " ] )
order . expires_at = timezone . now ( ) + timedelta (
seconds = order . t_to_expire ( Order . Status . SUC )
)
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . SUC )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
order . log (
f " Payment LNPayment( { lnpayment . payment_hash } , { str ( lnpayment ) } ) <b>succeeded</b> "
)
2023-05-22 14:56:15 +00:00
results = { " succeded " : True }
return results
except grpc . _channel . _InactiveRpcError as e :
if " code: Some " in str ( e ) :
status_code = int ( e . details ( ) . split ( " code: Some( " ) [ 1 ] . split ( " ) " ) [ 0 ] )
if (
status_code == 201
) : # Already paid with this hash using different amount or destination
# i don't think this can happen really, since we don't use the amount_msat in request
# and if you just try 'pay' 2x where the first time it succeeds you get the same
# non-error result the 2nd time.
print (
f " Order: { order . id } ALREADY PAID using different amount or destination THIS SHOULD NEVER HAPPEN! Hash: { hash } . "
)
# Permanent failure at destination. or Unable to find a route. or Route too expensive.
elif (
status_code == 203
or status_code == 205
or status_code == 206
or status_code == 210
) :
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . last_routing_time = timezone . now ( )
lnpayment . routing_attempts + = 1
lnpayment . failure_reason = LNPayment . FailureReason . NOROUTE
lnpayment . in_flight = False
if lnpayment . routing_attempts > 2 :
lnpayment . status = LNPayment . Status . EXPIRE
lnpayment . routing_attempts = 0
lnpayment . save (
update_fields = [
" status " ,
" last_routing_time " ,
" routing_attempts " ,
" in_flight " ,
" failure_reason " ,
]
)
order . expires_at = timezone . now ( ) + timedelta (
seconds = order . t_to_expire ( Order . Status . FAI )
)
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . FAI )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
2023-05-22 14:56:15 +00:00
print (
f " Order: { order . id } FAILED. Hash: { hash } Reason: { cls . payment_failure_context [ status_code ] } "
)
2023-08-06 17:48:20 +00:00
order . log (
f " Payment LNPayment( { lnpayment . payment_hash } , { str ( lnpayment ) } ) <b>failed</b>. Failure reason: { cls . payment_failure_context [ status_code ] } "
)
2023-05-22 14:56:15 +00:00
return {
" succeded " : False ,
" context " : f " payment failure reason: { cls . payment_failure_context [ status_code ] } " ,
}
elif status_code == 207 : # invoice expired
print ( f " Order: { order . id } . INVOICE EXPIRED. Hash: { hash } " )
last_payresponse = watchpayment ( )
# check if succeeded while pending and expired
if (
len ( last_payresponse . pays ) > 0
and last_payresponse . pays [ 0 ] . status
2023-11-08 14:25:34 +00:00
== node_pb2 . ListpaysPays . ListpaysPaysStatus . COMPLETE
2023-05-22 14:56:15 +00:00
) :
handle_response ( )
else :
lnpayment . status = LNPayment . Status . EXPIRE
lnpayment . last_routing_time = timezone . now ( )
lnpayment . in_flight = False
lnpayment . save (
update_fields = [
" status " ,
" last_routing_time " ,
" in_flight " ,
]
)
order . expires_at = timezone . now ( ) + timedelta (
seconds = order . t_to_expire ( Order . Status . FAI )
)
2024-08-26 13:49:03 +00:00
order . update_status ( Order . Status . FAI )
2023-08-06 17:48:20 +00:00
order . save ( update_fields = [ " expires_at " ] )
order . log (
f " Payment LNPayment( { lnpayment . payment_hash } , { str ( lnpayment ) } ) <b>had expired</b> "
)
2023-05-22 14:56:15 +00:00
results = {
" succeded " : False ,
" context " : " The payout invoice has expired " ,
}
return results
else : # -1 (general error)
print ( str ( e ) )
else :
print ( str ( e ) )
handle_response ( )
@classmethod
def send_keysend (
cls , target_pubkey , message , num_satoshis , routing_budget_sats , timeout , sign
) :
# keysends for dev donations
from api . models import LNPayment
# Cannot perform selfpayments
# config("ALLOW_SELF_KEYSEND", cast=bool, default=False)
keysend_payment = { }
keysend_payment [ " created_at " ] = timezone . now ( )
keysend_payment [ " expires_at " ] = timezone . now ( )
try :
custom_records = [ ]
msg = str ( message )
if len ( msg ) > 0 :
custom_records . append (
primitives__pb2 . TlvEntry (
type = 34349334 , value = bytes . fromhex ( msg . encode ( " utf-8 " ) . hex ( ) )
)
)
if sign :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
self_pubkey = nodestub . Getinfo ( node_pb2 . GetinfoRequest ( ) ) . id
2023-05-22 14:56:15 +00:00
timestamp = struct . pack ( " >i " , int ( time . time ( ) ) )
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
signature = nodestub . SignMessage (
2023-11-08 14:25:34 +00:00
node_pb2 . SignmessageRequest (
2023-05-22 14:56:15 +00:00
message = (
bytes . fromhex ( self_pubkey )
+ bytes . fromhex ( target_pubkey )
+ timestamp
+ bytes . fromhex ( msg . encode ( " utf-8 " ) . hex ( ) )
) ,
)
) . zbase
custom_records . append (
primitives__pb2 . TlvEntry ( type = 34349337 , value = signature )
)
custom_records . append (
primitives__pb2 . TlvEntry (
type = 34349339 , value = bytes . fromhex ( self_pubkey )
)
)
custom_records . append (
primitives__pb2 . TlvEntry ( type = 34349343 , value = timestamp )
)
# no maxfee for Keysend
maxfeepercent = ( routing_budget_sats / num_satoshis ) * 100
2023-11-08 14:25:34 +00:00
request = node_pb2 . KeysendRequest (
2023-05-22 14:56:15 +00:00
destination = bytes . fromhex ( target_pubkey ) ,
extratlvs = primitives__pb2 . TlvStream ( entries = custom_records ) ,
maxfeepercent = maxfeepercent ,
retry_for = timeout ,
amount_msat = primitives__pb2 . Amount ( msat = num_satoshis * 1000 ) ,
)
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
response = nodestub . KeySend ( request )
2023-05-22 14:56:15 +00:00
keysend_payment [ " preimage " ] = response . payment_preimage . hex ( )
keysend_payment [ " payment_hash " ] = response . payment_hash . hex ( )
2023-11-08 14:25:34 +00:00
waitreq = node_pb2 . WaitsendpayRequest (
2023-05-22 14:56:15 +00:00
payment_hash = response . payment_hash , timeout = timeout
)
try :
2023-11-08 14:56:36 +00:00
nodestub = node_pb2_grpc . NodeStub ( cls . node_channel )
waitresp = nodestub . WaitSendPay ( waitreq )
2023-05-22 14:56:15 +00:00
keysend_payment [ " fee " ] = (
float ( waitresp . amount_sent_msat . msat - waitresp . amount_msat . msat )
/ 1000
)
keysend_payment [ " status " ] = LNPayment . Status . SUCCED
except grpc . _channel . _InactiveRpcError as e :
if " code: Some " in str ( e ) :
status_code = int ( e . details ( ) . split ( " code: Some( " ) [ 1 ] . split ( " ) " ) [ 0 ] )
if status_code == 200 : # Timed out before the payment could complete.
keysend_payment [ " status " ] = LNPayment . Status . FLIGHT
elif status_code == 208 :
print (
f " A payment for { response . payment_hash . hex ( ) } was never made and there is nothing to wait for "
)
else :
keysend_payment [ " status " ] = LNPayment . Status . FAILRO
keysend_payment [ " failure_reason " ] = response . failure_reason
except Exception as e :
print ( " Error while sending keysend payment! Error: " + str ( e ) )
except Exception as e :
print ( " Error while sending keysend payment! Error: " + str ( e ) )
return True , keysend_payment
@classmethod
def double_check_htlc_is_settled ( cls , payment_hash ) :
""" Just as it sounds. Better safe than sorry! """
2023-11-08 14:25:34 +00:00
request = hold_pb2 . HoldInvoiceLookupRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( payment_hash )
)
try :
2023-11-08 14:56:36 +00:00
holdstub = hold_pb2_grpc . HoldStub ( cls . hold_channel )
response = holdstub . HoldInvoiceLookup ( request )
2023-05-22 14:56:15 +00:00
except Exception as e :
if " Timed out " in str ( e ) :
return False
else :
raise e
2024-06-17 21:33:49 +00:00
return response . state == hold_pb2 . Holdstate . SETTLED