2023-05-22 14:56:15 +00:00
import hashlib
import os
import secrets
import struct
import time
from base64 import b64decode
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 (
invoices_pb2 ,
invoices_pb2_grpc ,
lightning_pb2 ,
lightning_pb2_grpc ,
router_pb2 ,
router_pb2_grpc ,
signer_pb2 ,
signer_pb2_grpc ,
verrpc_pb2 ,
verrpc_pb2_grpc ,
)
2023-05-22 14:56:15 +00:00
#######
# Works with LND (c-lightning in the future for multi-vendor resilience)
#######
# Read tls.cert from file or .env variable string encoded as base64
try :
with open ( os . path . join ( config ( " LND_DIR " ) , " tls.cert " ) , " rb " ) as f :
CERT = f . read ( )
except Exception :
CERT = b64decode ( config ( " LND_CERT_BASE64 " ) )
# Read macaroon from file or .env variable string encoded as base64
try :
2023-11-12 12:39:39 +00:00
with open ( os . path . join ( config ( " LND_DIR " ) , config ( " MACAROON_PATH " ) ) , " rb " ) as f :
2023-05-22 14:56:15 +00:00
MACAROON = f . read ( )
except Exception :
MACAROON = b64decode ( config ( " LND_MACAROON_BASE64 " ) )
LND_GRPC_HOST = config ( " LND_GRPC_HOST " )
DISABLE_ONCHAIN = config ( " DISABLE_ONCHAIN " , cast = bool , default = True )
MAX_SWAP_AMOUNT = config ( " MAX_SWAP_AMOUNT " , cast = int , default = 500_000 )
2023-11-07 16:12:53 +00:00
# Logger function used to build tests/mocks/lnd.py
def log ( name , request , response ) :
2023-11-12 12:39:39 +00:00
if not config ( " LOG_LND " , cast = bool , default = False ) :
2023-11-07 16:12:53 +00:00
return
current_time = datetime . now ( ) . strftime ( " % Y- % m- %d % H: % M: % S " )
log_message = f " ###################################### \n Event: { name } \n Time: { current_time } \n Request: \n { request } \n Response: \n { response } \n Type: { type ( response ) } \n "
with open ( " lnd_log.txt " , " a " ) as file :
file . write ( log_message )
2023-05-22 14:56:15 +00:00
class LNDNode :
os . environ [ " GRPC_SSL_CIPHER_SUITES " ] = " HIGH+ECDSA "
def metadata_callback ( context , callback ) :
callback ( [ ( " macaroon " , MACAROON . hex ( ) ) ] , None )
ssl_creds = grpc . ssl_channel_credentials ( CERT )
auth_creds = grpc . metadata_call_credentials ( metadata_callback )
combined_creds = grpc . composite_channel_credentials ( ssl_creds , auth_creds )
channel = grpc . secure_channel ( LND_GRPC_HOST , combined_creds )
payment_failure_context = {
0 : " Payment isn ' t failed (yet) " ,
1 : " There are more routes to try, but the payment timeout was exceeded. " ,
2 : " All possible routes were tried and failed permanently. Or were no routes to the destination at all. " ,
3 : " A non-recoverable error has occured. " ,
4 : " Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta) " ,
5 : " Insufficient local balance. " ,
}
@classmethod
def get_version ( cls ) :
try :
2023-11-08 14:25:34 +00:00
request = verrpc_pb2 . VersionRequest ( )
verstub = verrpc_pb2_grpc . VersionerStub ( cls . channel )
response = verstub . GetVersion ( request )
2023-11-07 16:12:53 +00:00
log ( " verstub.GetVersion " , request , response )
2023-05-22 14:56:15 +00:00
return " v " + response . version
except Exception as e :
2023-11-17 15:16:03 +00:00
print ( f " Cannot get LND version: { e } " )
2023-11-14 01:42:04 +00:00
return " Not installed "
2023-05-22 14:56:15 +00:00
@classmethod
def decode_payreq ( cls , invoice ) :
""" Decodes a lightning payment request (invoice) """
2023-11-08 14:25:34 +00:00
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
request = lightning_pb2 . PayReqString ( pay_req = invoice )
response = lightningstub . DecodePayReq ( request )
log ( " lightning_pb2_grpc.DecodePayReq " , request , response )
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 """
2023-11-08 14:25:34 +00:00
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
request = lightning_pb2 . GetInfoRequest ( )
2023-11-07 16:12:53 +00:00
response = lightningstub . GetInfo ( request )
if response . testnet :
2023-10-27 16:34:20 +00:00
dummy_address = " tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x "
2023-11-15 19:48:04 +00:00
elif response . chains [ 0 ] . network == " regtest " :
dummy_address = " bcrt1q3w8xja7knmycsglnxg2xzjq8uv9u7jdwau25nl "
2023-10-27 16:34:20 +00:00
else :
dummy_address = " bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3 "
2023-05-22 14:56:15 +00:00
# We assume segwit. Use hardcoded address as shortcut so there is no need of user inputs yet.
2023-11-08 14:25:34 +00:00
request = lightning_pb2 . EstimateFeeRequest (
2023-10-27 16:34:20 +00:00
AddrToAmount = { dummy_address : amount_sats } ,
2023-05-22 14:56:15 +00:00
target_conf = target_conf ,
min_confs = min_confs ,
spend_unconfirmed = False ,
)
2023-11-08 14:25:34 +00:00
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
response = lightningstub . EstimateFee ( request )
log ( " lightning_pb2_grpc.EstimateFee " , request , response )
2023-05-22 14:56:15 +00:00
return {
" mining_fee_sats " : response . fee_sat ,
" mining_fee_rate " : response . sat_per_vbyte ,
}
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
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
request = lightning_pb2 . WalletBalanceRequest ( )
response = lightningstub . WalletBalance ( request )
log ( " lightning_pb2_grpc.WalletBalance " , request , response )
2023-05-22 14:56:15 +00:00
return {
" total_balance " : response . total_balance ,
" confirmed_balance " : response . confirmed_balance ,
" unconfirmed_balance " : response . 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
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
request = lightning_pb2 . ChannelBalanceRequest ( )
response = lightningstub . ChannelBalance ( request )
log ( " lightning_pb2_grpc.ChannelBalance " , request , response )
2023-05-22 14:56:15 +00:00
return {
" local_balance " : response . local_balance . sat ,
" remote_balance " : response . remote_balance . sat ,
" unsettled_local_balance " : response . unsettled_local_balance . sat ,
" unsettled_remote_balance " : response . unsettled_remote_balance . sat ,
}
@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 = lightning_pb2 . SendCoinsRequest (
2023-05-22 14:56:15 +00:00
addr = onchainpayment . address ,
amount = int ( onchainpayment . sent_satoshis ) ,
sat_per_vbyte = int ( onchainpayment . mining_fee_rate ) ,
label = str ( " Payout order # " + str ( onchainpayment . order_paid_TX . id ) ) ,
spend_unconfirmed = 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:25:34 +00:00
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
response = lightningstub . SendCoins ( request )
log ( " lightning_pb2_grpc.SendCoins " , request , response )
2023-05-22 14:56:15 +00:00
if response . txid :
onchainpayment . txid = response . txid
onchainpayment . broadcasted = True
onchainpayment . save ( update_fields = [ " txid " , " broadcasted " ] )
2023-08-06 17:48:20 +00:00
onchainpayment . order_paid_TX . log (
f " TX OnchainPayment( { onchainpayment . id } , { response . txid } ) in <b>mempool</b> "
)
2023-05-22 14:56:15 +00:00
return True
elif onchainpayment . status == on_mempool_code :
# Bug, double payment attempted
2023-08-06 17:48:20 +00:00
onchainpayment . order_paid_TX . log (
f " Attempted to re-broadcast OnchainPayment( { onchainpayment . id } , { onchainpayment } ) already in mempool " ,
level = " ERROR " ,
)
2023-05-22 14:56:15 +00:00
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 = invoices_pb2 . CancelInvoiceMsg (
payment_hash = bytes . fromhex ( payment_hash )
)
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . CancelInvoice ( request )
log ( " invoices_pb2_grpc.CancelInvoice " , request , response )
2023-05-22 14:56:15 +00:00
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
return str ( response ) == " " # True if no response, false otherwise.
@classmethod
def settle_hold_invoice ( cls , preimage ) :
""" settles a hold invoice """
2023-11-08 14:25:34 +00:00
request = invoices_pb2 . SettleInvoiceMsg ( preimage = bytes . fromhex ( preimage ) )
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . SettleInvoice ( request )
log ( " invoices_pb2_grpc.SettleInvoice " , request , response )
2023-05-22 14:56:15 +00:00
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
return str ( response ) == " " # True if no response, false otherwise.
@classmethod
2023-08-06 17:48:20 +00:00
def gen_hold_invoice (
cls ,
num_satoshis ,
description ,
invoice_expiry ,
cltv_expiry_blocks ,
order_id ,
lnpayment_concept ,
time ,
) :
2023-05-22 14:56:15 +00:00
""" Generates hold invoice """
hold_payment = { }
# The preimage is a random hash of 256 bits entropy
preimage = hashlib . sha256 ( secrets . token_bytes ( nbytes = 32 ) ) . digest ( )
# Its hash is used to generate the hold invoice
r_hash = hashlib . sha256 ( preimage ) . digest ( )
2023-11-08 14:25:34 +00:00
request = invoices_pb2 . AddHoldInvoiceRequest (
2023-05-22 14:56:15 +00:00
memo = description ,
value = num_satoshis ,
hash = r_hash ,
expiry = int (
invoice_expiry * 1.5
) , # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
cltv_expiry = cltv_expiry_blocks ,
)
2023-11-08 14:25:34 +00:00
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . AddHoldInvoice ( request )
log ( " invoices_pb2_grpc.AddHoldInvoice " , request , response )
2023-05-22 14:56:15 +00:00
hold_payment [ " invoice " ] = response . payment_request
payreq_decoded = cls . decode_payreq ( hold_payment [ " invoice " ] )
hold_payment [ " preimage " ] = preimage . hex ( )
hold_payment [ " payment_hash " ] = payreq_decoded . payment_hash
hold_payment [ " created_at " ] = timezone . make_aware (
datetime . fromtimestamp ( payreq_decoded . timestamp )
)
hold_payment [ " expires_at " ] = hold_payment [ " created_at " ] + timedelta (
seconds = payreq_decoded . expiry
)
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 = invoices_pb2 . LookupInvoiceMsg (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( lnpayment . payment_hash )
)
2023-11-08 14:25:34 +00:00
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . LookupInvoiceV2 ( request )
log ( " invoices_pb2_grpc.LookupInvoiceV2 " , request , response )
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)
2023-11-08 14:25:34 +00:00
if response . state == lightning_pb2 . Invoice . InvoiceState . OPEN : # OPEN
2023-05-22 14:56:15 +00:00
pass
2023-11-08 14:25:34 +00:00
if response . state == lightning_pb2 . Invoice . InvoiceState . SETTLED : # SETTLED
2023-05-22 14:56:15 +00:00
pass
2023-11-08 14:25:34 +00:00
if response . state == lightning_pb2 . Invoice . InvoiceState . CANCELED : # CANCELED
2023-05-22 14:56:15 +00:00
pass
2023-11-08 14:25:34 +00:00
if (
response . state == lightning_pb2 . Invoice . InvoiceState . ACCEPTED
) : # ACCEPTED (LOCKED)
2023-05-22 14:56:15 +00:00
lnpayment . expiry_height = response . htlcs [ 0 ] . expiry_height
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
lnd_response_state_to_lnpayment_status = {
0 : LNPayment . Status . INVGEN , # OPEN
1 : LNPayment . Status . SETLED , # SETTLED
2 : LNPayment . Status . CANCEL , # CANCELED
3 : LNPayment . Status . LOCKED , # ACCEPTED
}
try :
# this is similar to LNNnode.validate_hold_invoice_locked
2023-11-08 14:25:34 +00:00
request = invoices_pb2 . LookupInvoiceMsg (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( lnpayment . payment_hash )
)
2023-11-08 14:25:34 +00:00
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . LookupInvoiceV2 ( request )
log ( " invoices_pb2_grpc.LookupInvoiceV2 " , request , response )
2023-05-22 14:56:15 +00:00
status = lnd_response_state_to_lnpayment_status [ response . state ]
# get expiry height
if hasattr ( response , " htlcs " ) :
try :
for htlc in response . htlcs :
expiry_height = max ( expiry_height , htlc . expiry_height )
except Exception :
pass
except Exception as e :
# If it fails at finding the invoice: it has been canceled.
# In RoboSats DB we make a distinction between CANCELED and returned (LND does not)
if " unable to locate invoice " in str ( e ) :
print ( str ( e ) )
status = LNPayment . Status . CANCEL
# LND restarted.
if " wallet locked, unlock it " in str ( e ) :
print ( str ( timezone . now ( ) ) + " :: Wallet Locked " )
# Other write to logs
else :
print ( str ( e ) )
return status , expiry_height
2023-11-30 15:53:30 +00:00
# UNUSED
# @classmethod
# def resetmc(cls):
# routerstub = router_pb2_grpc.RouterStub(cls.channel)
# request = router_pb2.ResetMissionControlRequest()
# _ = routerstub.ResetMissionControl(request)
# return True
2023-05-22 14:56:15 +00:00
@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.
route_hints = payreq_decoded . route_hints
# 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 ) / 1_000_000
)
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 . hop_hints :
route_cost + = hop_hint . fee_base_msat / 1000
route_cost + = (
hop_hint . fee_proportional_millionths * num_satoshis / 1_000_000
)
# ...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 . num_satoshis == 0 :
payout [ " context " ] = {
" bad_invoice " : " The invoice provided has no explicit amount "
}
return payout
if not payreq_decoded . num_satoshis == num_satoshis :
payout [ " context " ] = {
" bad_invoice " : " The invoice provided is not for "
+ " {:,} " . format ( num_satoshis )
+ " Sats "
}
return payout
payout [ " created_at " ] = timezone . make_aware (
datetime . fromtimestamp ( payreq_decoded . timestamp )
)
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
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 = router_pb2 . SendPaymentRequest (
2023-05-22 14:56:15 +00:00
payment_request = lnpayment . invoice ,
fee_limit_sat = fee_limit_sat ,
timeout_seconds = timeout_seconds ,
2024-08-24 15:01:42 +00:00
amp = True ,
2023-05-22 14:56:15 +00:00
)
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . SendPaymentV2 ( request ) :
log ( " router_pb2_grpc.SendPaymentV2 " , request , response )
2023-05-22 14:56:15 +00:00
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . UNKNOWN
2023-05-22 14:56:15 +00:00
) : # Status 0 'UNKNOWN'
# Not sure when this status happens
pass
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . IN_FLIGHT
2023-05-22 14:56:15 +00:00
) : # Status 1 'IN_FLIGHT'
pass
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . FAILED
2023-05-22 14:56:15 +00:00
) : # Status 3 'FAILED'
""" 0 Payment isn ' t failed (yet).
1 There are more routes to try , but the payment timeout was exceeded .
2 All possible routes were tried and failed permanently . Or were no routes to the destination at all .
3 A non - recoverable error has occured .
4 Payment details incorrect ( unknown hash , invalid amt or invalid final cltv delta )
5 Insufficient local balance .
"""
failure_reason = cls . payment_failure_context [ response . failure_reason ]
lnpayment . failure_reason = response . failure_reason
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . save ( update_fields = [ " failure_reason " , " status " ] )
return False , failure_reason
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . SUCCEEDED
2023-05-22 14:56:15 +00:00
) : # STATUS 'SUCCEEDED'
lnpayment . status = LNPayment . Status . SUCCED
lnpayment . fee = float ( response . fee_msat ) / 1000
lnpayment . preimage = response . payment_preimage
lnpayment . save ( update_fields = [ " fee " , " status " , " preimage " ] )
return True , None
return False
@classmethod
def follow_send_payment ( cls , lnpayment , fee_limit_sat , timeout_seconds ) :
"""
Sends sats to buyer , continuous update .
Has a lot of boilerplate to correctly handle every possible condition and failure case .
"""
from api . models import LNPayment , Order
hash = lnpayment . payment_hash
2023-11-08 14:25:34 +00:00
request = router_pb2 . SendPaymentRequest (
2023-05-22 14:56:15 +00:00
payment_request = lnpayment . invoice ,
fee_limit_sat = fee_limit_sat ,
timeout_seconds = timeout_seconds ,
allow_self_payment = True ,
2024-08-24 15:01:42 +00:00
amp = True ,
2023-05-22 14:56:15 +00:00
)
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 handle_response ( response , was_in_transit = False ) :
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-05-22 14:56:15 +00:00
order . save ( update_fields = [ " status " ] )
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . UNKNOWN
2023-05-22 14:56:15 +00:00
) : # Status 0 'UNKNOWN'
# Not sure when this status happens
print ( f " Order: { order . id } UNKNOWN. Hash { hash } " )
lnpayment . in_flight = False
lnpayment . save ( update_fields = [ " in_flight " ] )
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . IN_FLIGHT
2023-05-22 14:56:15 +00:00
) : # Status 1 'IN_FLIGHT'
print ( f " Order: { order . id } IN_FLIGHT. Hash { hash } " )
# If payment was already "payment is in transition" we do not
# want to spawn a new thread every 3 minutes to check on it.
# in case this thread dies, let's move the last_routing_time
# 20 minutes in the future so another thread spawns.
if was_in_transit :
lnpayment . last_routing_time = timezone . now ( ) + timedelta ( minutes = 20 )
lnpayment . save ( update_fields = [ " last_routing_time " ] )
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . FAILED
2023-05-22 14:56:15 +00:00
) : # Status 3 'FAILED'
lnpayment . status = LNPayment . Status . FAILRO
lnpayment . last_routing_time = timezone . now ( )
lnpayment . routing_attempts + = 1
lnpayment . failure_reason = response . failure_reason
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 " ] )
str_failure_reason = cls . payment_failure_context [
response . failure_reason
]
2023-05-22 14:56:15 +00:00
print (
2023-08-06 17:48:20 +00:00
f " Order: { order . id } FAILED. Hash: { hash } Reason: { str_failure_reason } "
)
order . log (
f " Payment LNPayment( { lnpayment . payment_hash } , { str ( lnpayment ) } ) <b>failed</b>. Failure reason: { str_failure_reason } ) "
2023-05-22 14:56:15 +00:00
)
2023-08-06 17:48:20 +00:00
2023-05-22 14:56:15 +00:00
return {
" succeded " : False ,
" context " : f " payment failure reason: { cls . payment_failure_context [ response . failure_reason ] } " ,
}
if (
2023-11-08 14:25:34 +00:00
response . status == lightning_pb2 . Payment . PaymentStatus . SUCCEEDED
2023-05-22 14:56:15 +00:00
) : # Status 2 'SUCCEEDED'
print ( f " Order: { order . id } SUCCEEDED. Hash: { hash } " )
lnpayment . status = LNPayment . Status . SUCCED
lnpayment . fee = float ( response . fee_msat ) / 1000
lnpayment . preimage = response . payment_preimage
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
try :
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . SendPaymentV2 ( request ) :
log ( " router_pb2_grpc.SendPaymentV2 " , request , response )
2023-05-22 14:56:15 +00:00
handle_response ( response )
except Exception as e :
if " invoice expired " in str ( e ) :
print ( f " Order: { order . id } . INVOICE EXPIRED. Hash: { hash } " )
# An expired invoice can already be in-flight. Check.
try :
2023-11-08 14:25:34 +00:00
request = router_pb2 . TrackPaymentRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( hash )
)
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . TrackPaymentV2 ( request ) :
log ( " router_pb2_grpc.TrackPaymentV2 " , request , response )
2023-05-22 14:56:15 +00:00
handle_response ( response , was_in_transit = True )
except Exception as e :
if " payment isn ' t initiated " in str ( e ) :
print (
f " Order: { order . id } . The expired invoice had not been initiated. Hash: { hash } "
)
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
elif " payment is in transition " in str ( e ) :
print ( f " Order: { order . id } ALREADY IN TRANSITION. Hash: { hash } . " )
2023-11-08 14:25:34 +00:00
request = router_pb2 . TrackPaymentRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( hash )
)
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . TrackPaymentV2 ( request ) :
log ( " router_pb2_grpc.TrackPaymentV2 " , request , response )
2023-05-22 14:56:15 +00:00
handle_response ( response , was_in_transit = True )
elif " invoice is already paid " in str ( e ) :
print ( f " Order: { order . id } ALREADY PAID. Hash: { hash } . " )
2023-11-08 14:25:34 +00:00
request = router_pb2 . TrackPaymentRequest (
2023-05-22 14:56:15 +00:00
payment_hash = bytes . fromhex ( hash )
)
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . TrackPaymentV2 ( request ) :
log ( " router_pb2_grpc.TrackPaymentV2 " , request , response )
2023-05-22 14:56:15 +00:00
handle_response ( response )
else :
print ( str ( e ) )
@classmethod
def send_keysend (
cls , target_pubkey , message , num_satoshis , routing_budget_sats , timeout , sign
) :
# Thank you @cryptosharks131 / lndg for the inspiration
# Source https://github.com/cryptosharks131/lndg/blob/master/keysend.py
from api . models import LNPayment
ALLOW_SELF_KEYSEND = config ( " ALLOW_SELF_KEYSEND " , cast = bool , default = False )
keysend_payment = { }
keysend_payment [ " created_at " ] = timezone . now ( )
keysend_payment [ " expires_at " ] = timezone . now ( )
try :
secret = secrets . token_bytes ( 32 )
hashed_secret = hashlib . sha256 ( secret ) . hexdigest ( )
custom_records = [
( 5482373484 , secret ) ,
]
keysend_payment [ " preimage " ] = secret . hex ( )
keysend_payment [ " payment_hash " ] = hashed_secret
msg = str ( message )
if len ( msg ) > 0 :
custom_records . append (
( 34349334 , bytes . fromhex ( msg . encode ( " utf-8 " ) . hex ( ) ) )
)
if sign :
2023-11-08 14:25:34 +00:00
lightningstub = lightning_pb2_grpc . LightningStub ( cls . channel )
self_pubkey = lightningstub . GetInfo (
lightning_pb2 . GetInfoRequest ( )
2023-05-22 14:56:15 +00:00
) . identity_pubkey
timestamp = struct . pack ( " >i " , int ( time . time ( ) ) )
2023-11-08 14:25:34 +00:00
signerstub = signer_pb2_grpc . SignerStub ( cls . channel )
signature = signerstub . SignMessage (
signer_pb2 . SignMessageReq (
2023-05-22 14:56:15 +00:00
msg = (
bytes . fromhex ( self_pubkey )
+ bytes . fromhex ( target_pubkey )
+ timestamp
+ bytes . fromhex ( msg . encode ( " utf-8 " ) . hex ( ) )
) ,
2023-11-08 14:25:34 +00:00
key_loc = signer_pb2 . KeyLocator ( key_family = 6 , key_index = 0 ) ,
2023-05-22 14:56:15 +00:00
)
) . signature
custom_records . append ( ( 34349337 , signature ) )
custom_records . append ( ( 34349339 , bytes . fromhex ( self_pubkey ) ) )
custom_records . append ( ( 34349343 , timestamp ) )
2023-11-08 14:25:34 +00:00
request = router_pb2 . SendPaymentRequest (
2023-05-22 14:56:15 +00:00
dest = bytes . fromhex ( target_pubkey ) ,
dest_custom_records = custom_records ,
fee_limit_sat = routing_budget_sats ,
timeout_seconds = timeout ,
amt = num_satoshis ,
payment_hash = bytes . fromhex ( hashed_secret ) ,
allow_self_payment = ALLOW_SELF_KEYSEND ,
)
2023-11-08 14:25:34 +00:00
routerstub = router_pb2_grpc . RouterStub ( cls . channel )
for response in routerstub . SendPaymentV2 ( request ) :
log ( " router_pb2_grpc.SendPaymentV2 " , request , response )
if response . status == lightning_pb2 . Payment . PaymentStatus . IN_FLIGHT :
2023-05-22 14:56:15 +00:00
keysend_payment [ " status " ] = LNPayment . Status . FLIGHT
2023-11-08 14:25:34 +00:00
if response . status == lightning_pb2 . Payment . PaymentStatus . SUCCEEDED :
2023-05-22 14:56:15 +00:00
keysend_payment [ " fee " ] = float ( response . fee_msat ) / 1000
keysend_payment [ " status " ] = LNPayment . Status . SUCCED
2023-11-08 14:25:34 +00:00
if response . status == lightning_pb2 . Payment . PaymentStatus . FAILED :
2023-05-22 14:56:15 +00:00
keysend_payment [ " status " ] = LNPayment . Status . FAILRO
keysend_payment [ " failure_reason " ] = response . failure_reason
2023-11-08 14:25:34 +00:00
if response . status == lightning_pb2 . Payment . PaymentStatus . UNKNOWN :
2023-05-22 14:56:15 +00:00
print ( " Unknown Error " )
except Exception as e :
if " self-payments not allowed " in str ( e ) :
print ( " Self keysend is not allowed " )
else :
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 = invoices_pb2 . LookupInvoiceMsg (
payment_hash = bytes . fromhex ( payment_hash )
)
invoicesstub = invoices_pb2_grpc . InvoicesStub ( cls . channel )
response = invoicesstub . LookupInvoiceV2 ( request )
log ( " invoices_pb2_grpc.LookupInvoiceV2 " , request , response )
2023-05-22 14:56:15 +00:00
return (
2023-11-08 14:25:34 +00:00
response . state == lightning_pb2 . Invoice . InvoiceState . SETTLED
2023-05-22 14:56:15 +00:00
) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when CANCELED/returned