lndg/trade.py
2025-10-20 12:56:27 -04:00

1061 lines
No EOL
44 KiB
Python

import django, base64, secrets, re, asyncio, json
from django.db.models import Sum, IntegerField, Count, F, Q
from django.db.models.functions import Round
from time import time
from bech32 import bech32_decode, bech32_encode, convertbits
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from decimal import Decimal
from lndg import settings
from gui.lnd_deps import lightning_pb2 as ln
from gui.lnd_deps import lightning_pb2_grpc as lnrpc
from gui.lnd_deps import signer_pb2 as lns
from gui.lnd_deps import signer_pb2_grpc as lnsigner
from gui.lnd_deps import router_pb2 as lnr
from gui.lnd_deps import router_pb2_grpc as lnrouter
from gui.lnd_deps.lnd_connect import lnd_connect, async_lnd_connect
from os import environ
environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings'
django.setup()
from gui.models import TradeSales, Payments, PaymentHops, Forwards, Peers
import logging
logger = logging.getLogger('[P2P]')
def is_hex(n):
return len(n) % 2 == 0 and all(c in '0123456789ABCDEFabcdef' for c in n)
def hex_as_utf8(hex_str):
return bytes.fromhex(hex_str).decode('utf-8')
def utf8_as_hex(utf8_str):
return bytes(utf8_str, 'utf-8').hex()
def decode_basic_trade(records):
if not isinstance(records, list):
raise ValueError('ExpectedArrayOfRecordsToDecodeBasicTrade')
description_record = next((record for record in records if record['type'] == '2'), None)
if not description_record:
raise ValueError('ExpectedDescriptionRecordToDecodeBasicTrade')
id_record = next((record for record in records if record['type'] == '1'), None)
if not id_record:
raise ValueError('ExpectedIdRecordToDecodeBasicTradeDetails')
return {
'description': hex_as_utf8(description_record['value']),
'id': id_record['value']
}
def encode_as_bigsize(number: int):
max_8_bit_number = 252
max_16_bit_number = 65535
max_32_bit_number = 4294967295
def tag_as_uint8(n):
return format(n, '02x')
def tag_as_uint16(n):
return 'fd' + format(n, '04x')
def tag_as_uint32(n):
return 'fe' + format(n, '08x')
def tag_as_uint64(n):
return 'ff' + format(n, '016x')
number = int(number)
if number <= max_8_bit_number:
return tag_as_uint8(number)
elif number <= max_16_bit_number:
return tag_as_uint16(number)
elif number <= max_32_bit_number:
return tag_as_uint32(number)
else:
return tag_as_uint64(number)
def decode_as_bigsize(encoded_string: str):
def read_uint8(encoded_string):
return int(encoded_string[:2], 16), encoded_string[2:]
def read_uint16(encoded_string):
return int(encoded_string[2:6], 16), encoded_string[6:]
def read_uint32(encoded_string):
return int(encoded_string[2:10], 16), encoded_string[10:]
def read_uint64(encoded_string):
return int(encoded_string[2:18], 16), encoded_string[18:]
if encoded_string.startswith('ff'):
value, remaining = read_uint64(encoded_string)
elif encoded_string.startswith('fe'):
value, remaining = read_uint32(encoded_string)
elif encoded_string.startswith('fd'):
value, remaining = read_uint16(encoded_string)
else:
value, remaining = read_uint8(encoded_string)
return value
def decode_big_size(encoded):
max_8bit_number = 0xfc
max_16bit_number = 0xffff
max_32bit_number = 0xffffffff
uint8_length = 1
uint16 = 0xfd
uint16_length = 3
uint32 = 0xfe
uint32_length = 5
uint64_length = 9
if not bool(encoded) and is_hex(encoded):
raise ValueError('ExpectedHexEncodedBigSizeValueToDecode')
bytes_data = bytes.fromhex(encoded)
size = bytes_data[0]
if size <= max_8bit_number:
return {'decoded': str(size), 'length': uint8_length}
byte_length = len(bytes_data)
if size == uint16:
if byte_length < uint16_length:
raise ValueError('ExpectedMoreBytesToDecodeUint16BigSize')
uint16_number = int.from_bytes(bytes_data[1:3], 'big')
if uint16_number <= max_8bit_number:
raise ValueError('ExpectedLargerNumberToDecodeUint16BigSize')
return {'decoded': str(uint16_number), 'length': uint16_length}
elif size == uint32:
if byte_length < uint32_length:
raise ValueError('ExpectedMoreBytesToDecodeUint32BigSize')
uint32_number = int.from_bytes(bytes_data[1:5], 'big')
if uint32_number <= max_16bit_number:
raise ValueError('ExpectedLargerNumberToDecodeUint32BigSize')
return {'decoded': str(uint32_number), 'length': uint32_length}
else:
if byte_length < uint64_length:
raise ValueError('ExpectedMoreBytesToDecodeUint64BigSize')
uint64_number = int.from_bytes(bytes_data[1:9], 'big')
if uint64_number <= max_32bit_number:
raise ValueError('ExpectedLargerNumberToDecodeUint64BigSize')
return {'decoded': str(uint64_number), 'length': uint64_length}
def decode_tlv_record(data):
def hex_len(byte_length):
return 0 if not byte_length else byte_length * 2
def read(from_pos, hex_str, to_pos=None):
if to_pos is None:
return hex_str[from_pos:]
else:
return hex_str[from_pos:to_pos + from_pos]
encoded = data['encoded']
offset = data.get('offset', 0)
start = hex_len(offset)
type_record = decode_big_size(read(start, encoded))
size_start = start + hex_len(type_record['length'])
bytes_record = decode_big_size(read(size_start, encoded))
meta_bytes = type_record['length'] + bytes_record['length']
if not int(bytes_record['decoded']):
return {'length': meta_bytes, 'type': type_record['decoded'], 'value': ''}
value_start = start + hex_len(meta_bytes)
total_bytes = meta_bytes + int(bytes_record['decoded'])
if start + hex_len(total_bytes) > len(encoded):
raise ValueError('ExpectedAdditionalValueBytesInTlvRecord')
return {
'length': total_bytes,
'type': type_record['decoded'],
'value': read(value_start, encoded, hex_len(int(bytes_record['decoded'])))
}
def decode_tlv_stream(encoded):
if not is_hex(encoded):
raise ValueError('ExpectedHexEncodedTlvStreamToDecode')
if not encoded:
return []
total_bytes = len(encoded) // 2
stream = {'offset': 0, 'records': []}
while stream['offset'] < total_bytes:
stream['record'] = decode_tlv_record({'encoded': encoded, 'offset': stream['offset']})
stream['offset'] += stream['record']['length']
stream['records'].append(stream['record'])
return stream['records']
def parse_response_code(data):
encoded = data['encoded']
if not encoded:
raise ValueError('ExpectedResponseCodeValueToParseResponseCode')
records = decode_tlv_stream(encoded)
code_record = next((record for record in records if record['type'] == '0'), None)
if not code_record:
raise ValueError('ExpectedCodeRecordToParseResponseCode')
code = int(decode_big_size(code_record['value'])['decoded'])
if code > 2 ** 53 - 1:
raise ValueError('UnexpectedlyLargeResponseCodeInResponse')
if code < 100:
raise ValueError('UnexpectedlySmallResponseCodeInResponse')
if not code > 400:
return {}
message_record = next((record for record in records if record['type'] == '1'), None) or {'value': ''}
return {'failure': [code, hex_as_utf8(message_record['value'])]}
def parse_peer_request_message(message):
records = decode_tlv_stream(message[len('626f73ff'):])
version = next((record for record in records if record['type'] == '0'), None)
if version is not None:
raise ValueError('UnexpectedVersionNumberOfRequestMessage')
id_record = next((record for record in records if record['type'] == '1'), None)
if id_record is None or len(id_record['value']) != 64:
raise ValueError('ExpectedRequestIdInRequestMessage')
records_record = next((record for record in records if record['type'] == '5'), None) or {'value':''}
response_code_record = next((record for record in records if record['type'] == '2'), None)
type_record = next((record for record in records if record['type'] == '3'), None)
if not type_record and not response_code_record:
raise ValueError('ExpectedEitherRequestParametersOrResponseCode')
if response_code_record is not None:
response_code = parse_response_code({'encoded': response_code_record['value']})
failure = response_code['failure'] if 'failure' in response_code else None
return {
'response': {
'failure': failure,
'id': id_record['value'],
'records': [{'type': record['type'], 'value': record['value']} for record in decode_tlv_stream(records_record['value'])],
},
}
else:
return {
'request': {
'id': id_record['value'],
'records': [{'type': record['type'], 'value': record['value']} for record in decode_tlv_stream(records_record['value'])],
'type': decode_big_size(type_record['value'])['decoded'],
},
}
def decode_basic_trade(records):
if not isinstance(records, list):
raise ValueError('ExpectedArrayOfRecordsToDecodeBasicTrade')
description_record = next((record for record in records if record['type'] == '2'), None)
if not description_record:
raise ValueError('ExpectedDescriptionRecordToDecodeBasicTrade')
id_record = next((record for record in records if record['type'] == '1'), None)
if not id_record:
raise ValueError('ExpectedIdRecordToDecodeBasicTradeDetails')
return {
'description': hex_as_utf8(description_record['value']),
'id': id_record['value']
}
def decode_anchored_trade_data(encoded):
anchor_prefix = 'anchor-trade-secret:'
if not encoded.startswith(anchor_prefix):
return {}
encoded_data = encoded[len(anchor_prefix):]
try:
decoded_data = decode_tlv_stream(base64.b64decode(encoded_data).hex())
except Exception as e:
return {}
records = decoded_data
channel_record = next((record for record in records if record['type'] == '3'), None)
description_record = next((record for record in records if record['type'] == '1'), None)
secret_record = next((record for record in records if record['type'] == '0'), None)
price_record = next((record for record in records if record['type'] == '2'), None)
if channel_record:
try:
channel_value = int(decode_big_size({'encoded': channel_record['value']})['decoded'])
except ValueError:
return {}
return {
'channel': channel_value,
'price': hex_as_utf8(price_record['value']) if price_record else None,
}
if description_record and secret_record:
return {
'description': hex_as_utf8(description_record['value']),
'price': hex_as_utf8(price_record['value']) if price_record else None,
'secret': hex_as_utf8(secret_record['value']),
}
return None
def encode_tlv_record(data):
def byte_length_of(hex_string):
return len(hex_string) // 2
def encode(type, length, val):
return f"{type}{length}{val}"
type_number = data["type"]
value_hex = data["value"]
data_length = encode_as_bigsize(byte_length_of(value_hex))
encoded_tlv_record = encode(encode_as_bigsize(type_number), data_length, value_hex)
return encoded_tlv_record
def encode_peer_request(data):
id = data['id']
records = data.get('records')
request_type = data['type']
if not id:
raise ValueError('ExpectedRequestIdHexStringToEncodePeerRequest')
if records is not None and not isinstance(records, list):
raise ValueError('ExpectedRecordsArrayToEncodePeerRequest')
if not request_type:
raise ValueError('ExpectedRequestTypeToEncodePeerRequest')
peer_response = [{'type': '1', 'value': id},{'type': '3', 'value': encode_as_bigsize(request_type)},{'type': '5', 'value': records}]
peer_response = [
{'type': item['type'], 'value': item['value']} if not isinstance(item['value'], list) else {'type': item['type'], 'value': ''.join([encode_tlv_record(record) for record in item['value']])}
for item in peer_response
]
peer_response = [{'type': item['type'], 'value': str(item['value'])} for item in peer_response]
return '626f73ff' + ''.join([encode_tlv_record(record) for record in peer_response])
def encode_response_code(data):
code_for_success = 200
failure = data.get('failure')
if not failure:
return ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': encode_as_bigsize(code_for_success)}]])
if not isinstance(failure, list):
raise ValueError('ExpectedFailureArrayToEncodeResponseCode')
code, message = failure
if not code:
raise ValueError('ExpectedErrorCodeToEncodeResponseCode')
records = [{'value': encode_as_bigsize(code), 'type': '0'}, {'value': utf8_as_hex(message), 'type': '1'}]
return ''.join([encode_tlv_record(record) for record in records])
def encode_peer_response(data):
failure = data.get('failure')
id = data['id']
records = data.get('records')
code = encode_response_code({'failure': failure})
encoded = ''.join([encode_tlv_record(record) for record in records]) if records is not None else None
peer_response = [{'type': '1', 'value': id}, {'type': '2', 'value': code}, {'type': '5', 'value': encoded}]
return '626f73ff' + ''.join([encode_tlv_record(record) for record in peer_response])
def get_legacy_trades(stub):
trades = []
for invoice in stub.ListInvoices(ln.ListInvoiceRequest(pending_only=True)).invoices:
if invoice.is_keysend == False:
trade = decode_anchored_trade_data(invoice.memo)
if trade:
trade['price'] = invoice.value
trade['id'] = invoice.r_hash.hex()
trade['expiry'] = invoice.expiry
trade['creation_date'] = invoice.creation_date
trades.append(trade)
return trades
def get_trades(trade_id=None):
trades = TradeSales.objects.filter(Q(expiry__isnull=True) | Q(expiry__gt=datetime.now())).filter(Q(sale_limit__isnull=True) | Q(sale_count__lt=F('sale_limit')))
if trade_id:
trades = trades.filter(id=trade_id)
return trades
def decodePrefix(prefix):
bech32CurrencyCodes={"bc": "bitcoin","bcrt": "regtest","ltc": "litecoin","tb": "testnet","tbs": "signet","sb": "simnet"}
matches = re.compile(r'^ln(\S+?)(\d*)([a-zA-Z]?)$').match(prefix)
if not matches or not matches.groups():
raise ValueError('InvalidPaymentRequestPrefix')
_, _, type = matches.groups()
prefixElements = re.compile(r'^ln(\S+)$').match(prefix) if not type else matches
currency, amount, units = prefixElements.groups()
network = bech32CurrencyCodes.get(currency)
if not network:
raise ValueError('UnknownCurrencyCodeInPaymentRequest')
return amount, network, units
def parseHumanReadableValue(data):
amountMultiplierPattern = r'^[^munp0-9]$'
divisibilityMarkerLen = 1
divisibilityPattern = r'^[munp]$'
amount = data.get('amount')
units = data.get('units')
hrp = f"{amount}{units}"
if re.match(divisibilityPattern, hrp[-divisibilityMarkerLen:]):
return {
'divisor': hrp[-divisibilityMarkerLen:],
'value': hrp[:-(divisibilityMarkerLen)],
}
if re.match(amountMultiplierPattern, hrp[-divisibilityMarkerLen:]):
raise ValueError('InvalidAmountMultiplier')
return {'value': hrp}
def hrpAsMtokens(amount, units):
divisors ={"m": "1000","n": "1000000000","p": "1000000000000","u": "1000000"}
if not amount:
return {}
result = parseHumanReadableValue({'amount': amount, 'units': units})
divisor = result['divisor']
value = result['value']
if not bool(re.match(r'^\d+$', value)):
raise ValueError('ExpectedValidNumericAmountToParseHrpAsMtokens')
val = Decimal(value)
if not divisor:
return {'mtokens': str(int(val * Decimal(1e11)))}
div = Decimal(divisors.get(divisor, 1))
return str(int(val * Decimal(1e11) / div))
def mtokensAsHrp(mtokens):
amount = int(mtokens)
hrp = None
multipliers = {
'n': 100,
'u': 100000,
'm': 100000000,
'': 100000000000,
}
for letter, value in multipliers.items():
value = int(value)
if amount % value == 0:
if letter == 'u':
hrp = f"{amount // value}u"
elif letter == 'm':
hrp = f"{amount // value}m"
elif letter == 'n':
hrp = f"{amount // value}n"
elif letter == '':
hrp = f"{amount // value}"
if not hrp:
return str(amount * 10) + 'p'
return hrp
def decodeBech32Words(words):
inBits = 5
outBits = 8
bits = 0
maxV = (1 << outBits) - 1
result = []
value = 0
for word in words:
value = (value << inBits) | word
bits += inBits
while bits >= outBits:
bits -= outBits
result.append((value >> bits) & maxV)
if bits:
result.append((value << (outBits - bits)) & maxV)
return bytes(result)
def byteEncodeRequest(request):
if not request:
raise ValueError('ExpectedPaymentRequestToByteEncode')
if request[:2].lower() != 'ln':
raise ValueError('ExpectedLnPrefixToByteEncodePaymentRequest')
(prefix, words) = bech32_decode(request)
(amount, network, units) = decodePrefix(prefix)
mtokens = hrpAsMtokens(amount, units)
encoded = decodeBech32Words(words).hex()
return {'encoded': encoded, 'network': network, 'mtokens': mtokens, 'words': len(words)}
def byteDecodeRequest(encoded, mtokens, network, words):
if not is_hex(encoded):
raise ValueError('ExpectedHexEncodedPaymentRequestDataToDecodeRequest')
if not network:
raise ValueError('ExpectedNetworkToDecodeByteEncodedRequest')
if not words:
raise ValueError('ExpectedWordsCountToDecodeByteEncodedRequest')
if network == 'bitcoin':
prefix = 'bc'
elif network == 'testnet':
prefix = 'tb'
elif network == 'regtest':
prefix = 'bcrt'
elif network == 'signet':
prefix = 'tbs'
else:
raise ValueError('ExpectedKnownNetworkToDecodeByteEncodedRequest')
prefix = 'ln' + prefix + mtokensAsHrp(mtokens)
five_bit = convertbits(bytes.fromhex(encoded), 8, 5)[:words]
return bech32_encode(prefix, five_bit)
def encode_request_as_records(request):
if not request:
raise ValueError('ExpectedRequestToEncodeAsRequestRecords')
records = []
result = byteEncodeRequest(request)
encoded = result['encoded']
mtokens = result['mtokens']
words = result['words']
records.append({'type': '1', 'value': encoded})
records.append({'type': '0', 'value': encode_as_bigsize(words)})
if mtokens:
records.append({'type': '2', 'value': encode_as_bigsize(mtokens)})
return ''.join([encode_tlv_record(record) for record in records]), result['network']
def decode_records_as_request(encoded, network):
if not encoded:
raise ValueError('ExpectectedEncodedPaymentRequestRecordsToDecode')
if not network:
raise ValueError('ExpectedNetworkNameToDeriveRequestFromRequestRecords')
records = decode_tlv_stream(encoded)
word_count = next((record for record in records if record['type'] == '0'), None)
if not word_count:
raise ValueError('ExpectedWordCountRecordInPaymentTlvRecord')
try:
words = int(decode_as_bigsize(word_count['value']))
except:
raise ValueError('ExpectedPaymentRequestWordCountInRequestRecords')
details = next((record for record in records if record['type'] == '1'), None)
if not details:
raise ValueError('ExpectedEncodedPaymentDetailsInPaymentTlvRecord')
amount = next((record for record in records if record['type'] == '2'), None)
if not amount:
raise ValueError('ExpectedPaymentRequestTokensInPaymentRecords')
mtokens = decode_as_bigsize(amount['value'])
try:
request = byteDecodeRequest(details['value'], mtokens, network, words)
except:
raise ValueError('ExpectedValidPaymentRequestDetailsToDecodeRecords')
return request
def encode_final_trade(auth, payload, request):
if not request:
raise ValueError('ExpectedPaymentRequestToDeriveNetworkRecord')
encoded_request, network = encode_request_as_records(request)
trade_records = [{'type': '2', 'value': encoded_request}]
if settings.LND_NETWORK != 'mainnet':
network_value = '02' if network == 'regtest' else '01'
trade_records.append({'type': '1','value': network_value})
encryption_records = ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': payload}, {'type': '1', 'value': auth}]])
details_records = ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': encryption_records}]])
trade_records.append({'type': '3', 'value': details_records})
return '626f73ff' + ''.join([encode_tlv_record(record) for record in trade_records])
def decode_final_trade(network, request, details):
details = decode_tlv_stream(details['value'])
encrypted = next((record for record in details if record['type'] == '0'), None)
if not encrypted:
raise ValueError('ExpectedEncryptedRecordToDecodeTrade')
encrypted_records = decode_tlv_stream(encrypted['value'])
encrypted_data = next((record for record in encrypted_records if record['type'] == '0'), None)
if not encrypted_data:
raise ValueError('ExpectedEncryptedDataRecordToDecodeTrade')
auth = next((record for record in encrypted_records if record['type'] == '1'), None)
if not auth:
raise ValueError('ExpectedAuthDataRecordToDecodeTrade')
return decode_records_as_request(request['value'], network), auth['value'], encrypted_data['value']
def getSecret(stub, sale_type):
if sale_type == 1: # routing data
try:
filter_30day = datetime.now() - timedelta(days=30)
incoming_nodes = Forwards.objects.filter(forward_date__gte=filter_30day).values('chan_id_in').annotate(ppm=Round((Sum('fee')/Sum('amt_in_msat'))*1000000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt_in_msat')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', '-ppm')[:5]
outgoing_nodes = Forwards.objects.filter(forward_date__gte=filter_30day).values('chan_id_out').annotate(ppm=Round((Sum('fee')/Sum('amt_out_msat'))*1000000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt_out_msat')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', '-ppm')[:5]
secret = json.dumps({"incoming_nodes":list(incoming_nodes.values('chan_id_in', 'score', 'ppm')), "outgoing_nodes":list(outgoing_nodes.values('chan_id_out', 'score', 'ppm'))})
except Exception as e:
logger.error(f'Error getting secret: {str(e)}')
secret = None
finally:
return secret
elif sale_type == 2: # payment data
try:
self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey
filter_30day = datetime.now() - timedelta(days=30)
# exlcude_list = AvoidNodes.objects.values_list('pubkey')
payments_30day = Payments.objects.filter(creation_date__gte=filter_30day, status=2).values_list('payment_hash')
payment_nodes = PaymentHops.objects.filter(payment_hash__in=payments_30day).exclude(node_pubkey=self_pubkey).values('node_pubkey').annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', 'ppm')[:10]
secret = json.dumps({"payment_nodes": list(payment_nodes.values('node_pubkey', 'score', 'ppm'))})
except Exception as e:
logger.error(f'Error getting secret: {str(e)}')
secret = None
finally:
return secret
else:
return None
def serve_trades(stub):
logger.info('Serving trades...')
for trade in get_trades():
logger.info(f'Serving trade: {trade.id}')
for response in stub.SubscribeCustomMessages(ln.SubscribeCustomMessagesRequest()):
if response.type == 32768:
from_peer = response.peer
msg_type = response.type
message = response.data.hex()
if msg_type == 32768 and message.lower().startswith('626f73ff'):
msg_response = parse_peer_request_message(message)
if 'request' in msg_response:
request = msg_response['request']
if 'type' in request:
req_type = request['type']
if req_type == '8050005': # request a seller to finalize a trade or give all open trades
logger.info(f'SELLER ACTION | ID: {request["id"]} | Records: {request["records"]}')
select_trade = next((record for record in request['records'] if record['type'] == '0'), None)
request_trade = next((record for record in request['records'] if record['type'] == '1'), None)
if request_trade:
trades = get_trades()
for trade in trades:
trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050006', 'records':[{'type': '1', 'value': trade.id}, {'type': '2', 'value': utf8_as_hex(trade.description)}, {'type': '0', 'value': request_trade['value']}]})
stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(trade_data)))
ack_data = encode_peer_response({'failure':None, 'id':request['id'], 'records':[]})
for trade in trades:
stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(ack_data)))
if select_trade:
selected_trade = get_trades(trade.id)
if selected_trade:
trade_details = selected_trade[0]
if not trade_details.secret:
secret = getSecret(stub, trade_details.sale_type)
else:
secret = trade_details.secret
if not secret:
logger.error(f'Failed to get secret for: {trade_details.id}')
continue
signerstub = lnsigner.SignerStub(lnd_connect())
shared_key = signerstub.DeriveSharedKey(lns.SharedKeyRequest(ephemeral_pubkey=from_peer)).shared_key
preimage = secrets.token_bytes(32)
shared_secret = bytes(x ^ y for x, y in zip(shared_key, preimage))
cipher = Cipher(algorithms.AES(shared_secret), modes.GCM(bytes(16)), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = (encryptor.update(secret.encode('utf-8')) + encryptor.finalize()).hex()
auth_tag = encryptor.tag.hex()
time_to_expiry = (trade_details.expiry-datetime.now()).seconds if trade_details.expiry else None
default_expiry = 30*60
inv_expiry = default_expiry if time_to_expiry is None or time_to_expiry > default_expiry else time_to_expiry
if inv_expiry > 0:
final_invoice = stub.AddInvoice(ln.Invoice(memo=trade_details.description, value=trade_details.price, expiry=inv_expiry, r_preimage=preimage))
trade_data = encode_peer_response({'failure': None, 'id':request['id'], 'records': [{'type':'1', 'value':encode_final_trade(auth_tag, ciphertext, final_invoice.payment_request)}]})
trade_details.sale_count = F('sale_count') + 1
trade_details.save()
stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(trade_data)))
else:
logger.error(f'Expected request type in message: {request["id"]}')
if 'response' in msg_response:
request = msg_response['response']
if 'failure' in request and request['failure'] != None:
# failure message returned
logger.error(f'Failure: {request["failure"]}')
else:
if len(request['records']) == 0:
# message acknowledgements
logger.info(f'ACK | ID: {request["id"]}')
async def get_open_trades(astub, results):
try:
async for response in astub.SubscribeCustomMessages(ln.SubscribeCustomMessagesRequest()):
if response.type == 32768:
# from_peer = response.peer
msg_type = response.type
message = response.data.hex()
if msg_type == 32768 and message.lower().startswith('626f73ff'):
msg_response = parse_peer_request_message(message)
if 'request' in msg_response:
request = msg_response['request']
if 'type' in request:
req_type = request['type']
if req_type == '8050006': # request a buyer to select the trade provided
# select a trade from the records
trade = decode_basic_trade(request['records'])
print('BUYER ACTION', '|', 'ID:', request['id'], '|', 'Trade:', trade)
results.append({'id': request['id'], 'trade': trade})
else:
raise ValueError('ExpectedRequestTypeInRequestMessage')
if 'response' in msg_response:
request = msg_response['response']
if 'failure' in request and request['failure'] != None:
# failure message returned
print('Failure:', request['failure'])
return
else:
if len(request['records']) == 0:
# message acknowledgements
print('ACK', '|', 'ID:', request['id'])
else:
# buyer to pay invoice and get secret their secret from preimage
print('BUYER FINALIZE', '|', 'ID:', request['id'], '|', 'Records:', request['records'])
return request['records']
except asyncio.CancelledError:
pass
except Exception as e:
print('Error runnig task:', str(e))
def encode_nodes_data(data, network_value):
records = [{'type': '0', 'value': '01'}]
if network_value:
records.append({'type': '1', 'value': network_value})
node_records = []
for idx, node in enumerate(data):
node_record = [{ 'type': '2', 'value': node['id'] }]
encoded_node_record = ''.join([encode_tlv_record(record) for record in node_record])
node_records.append({'type': str(idx), 'value': encoded_node_record})
records.append({'type': '4', 'value': ''.join([encode_tlv_record(record) for record in node_records])})
nodes_encoded = '626f73ff' + ''.join([encode_tlv_record(record) for record in records])
return nodes_encoded
def decode_node_record(encoded):
channel_hex_length = 16
if not encoded:
raise ValueError('ExpectedEncodedNodeRecordToGetNodePointer')
records = decode_tlv_stream(encoded['value'])
high_key_record = next((n for n in records if n['type'] == '1'), None)
if high_key_record and len(high_key_record['value']) != channel_hex_length:
raise ValueError('ExpectedChannelIdInHighKeyRecord')
if high_key_record:
return {'high_channel': high_key_record['value']}
low_key_record = next((n for n in records if n['type'] == '0'), None)
if low_key_record and len(low_key_record['value']) != channel_hex_length:
raise ValueError('ExpectedChannelIdInLowKeyRecord')
if low_key_record:
return {'low_channel': low_key_record['value']}
id_record = next((n for n in records if n['type'] == '2'), None)
if not id_record:
raise ValueError('ExpectedNodeIdRecordToMapNodeRecordToNodePointer')
if not (bool(id_record['value']) and re.match(r'^0[2-3][0-9A-F]{64}$', id_record['value'], re.I)):
raise ValueError('ExpectedNodeIdPublicKeyToMapNodeRecordToNodePointer')
return {'node': {'id': id_record['value']}}
def decode_open_trade(network, records):
if not network:
raise ValueError('ExpectedNetworkNameToDecodeOpenTrade')
if not isinstance(records, list):
raise ValueError('ExpectedArrayOfRecordsToDecodeOpenTrade')
nodes_record = next((n for n in records if n['type'] == '4'), None)
id_record = next((n for n in records if n['type'] == '5'), None)
if not nodes_record:
raise ValueError('ExpectedNodesRecordToDecodeOpenTradeDetails')
try:
decode_tlv_stream(nodes_record['value'])
except:
raise ValueError('ExpectedValidNodesTlvStreamToDecodeOpenTradeDetails')
node_records = decode_tlv_stream(nodes_record['value'])
if not node_records:
raise ValueError('ExpectedNodeRecordsForOpenTrade')
return network, id_record['value'] if id_record else None, [decode_node_record(value) for value in node_records]
def decode_trade_data(encoded):
if not encoded.lower().startswith('626f73ff'):
raise ValueError('UnexpectedFormatOfTradeToDecode')
try:
decoded_trade = decode_tlv_stream(encoded[8:])
except:
raise ValueError('ExpectedValidTlvStreamForTradeData')
records = decoded_trade
network_value = next((record for record in records if record['type'] == '1'), None)
if network_value:
if network_value['value'] == '01':
if settings.LND_NETWORK == 'testnet':
network = 'testnet'
else:
raise ValueError('TradeRequestForAnotherNetwork')
elif network_value['value'] =='02':
if settings.LND_NETWORK == 'regtest':
network = 'regtest'
else:
raise ValueError('TradeRequestForAnotherNetwork')
else:
raise ValueError('UnknownNetworkNameToDeriveNetworkRecordFor')
else:
if settings.LND_NETWORK == 'mainnet':
network = 'bitcoin'
else:
raise ValueError('TradeRequestForAnotherNetwork')
request = next((record for record in records if record['type'] == '2'), None)
details = next((record for record in records if record['type'] == '3'), None)
swap = next((record for record in records if record['type'] == '6'), None)
if request and details:
return {'secret': decode_final_trade(network, request, details)}
elif request:
pass # just a payment
elif swap:
pass # swap offer
else:
return {'connect': decode_open_trade(network, records)}
def encode_trade(description, price, secret):
anchorPrefix = 'anchor-trade-secret:'
elements = [
{'value': secret, 'type': '0'},
{'value': description, 'type': '1'},
{'value': price, 'type': '2'},
]
records = [
{'type': record['type'], 'value': bytes(record['value'], 'utf-8').hex()}
for record in elements if record
]
encoded_data = ''.join([encode_tlv_record(record) for record in records])
encoded = anchorPrefix + base64.b64encode(bytes.fromhex(encoded_data)).decode()
return encoded
def create_trade_details(stub):
nodes = [{'id':stub.GetInfo(ln.GetInfoRequest()).identity_pubkey}]
lnd_network = settings.LND_NETWORK
if lnd_network == 'mainnet':
network_value = None
elif lnd_network == 'testnet':
network_value = '01'
elif lnd_network == 'regtest':
network_value = '02'
else:
raise ValueError('UnsupportedNetworkForTrades')
return encode_nodes_data(nodes, network_value)
def create_trade_anchor(stub, description, price, secret, expiry):
try:
encoded_trade = encode_trade(description, price, secret)
stub.AddInvoice(ln.Invoice(value=int(price), expiry=int(expiry)*60*60*24, memo=encoded_trade))
except Exception as e:
print('Error creating trade:', str(e))
print('Trade Anchor:', encoded_trade)
async def request_trades(to_peer):
results = []
start_time = time()
astub = lnrpc.LightningStub(async_lnd_connect())
task = asyncio.create_task(get_open_trades(astub, results))
asyncio.gather(task)
trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050005', 'records':[{'type': '1', 'value': secrets.token_bytes(32).hex()}]})
astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(trade_data)))
while len(results) == 0:
if (time() - start_time) < 30:
await asyncio.sleep(1)
else:
print('Timeout waiting for trade records from peer.')
task.cancel()
return None
await asyncio.sleep(1)
if len(results) == 1:
ack_data = encode_peer_response({'failure':None, 'id': results[0]['id'], 'records':[]})
astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(ack_data)))
choice = 1
else:
print('Select a trade to buy:')
for idx, trade in enumerate(results, start=1):
ack_data = encode_peer_response({'failure':None, 'id': trade['id'], 'records':[]})
astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(ack_data)))
print(f"{idx}. {trade['trade']['description']}")
while True:
try:
choice = int(input("Enter the number of your choice: "))
if 1 <= choice <= len(results):
break
else:
print("Invalid input. Please enter a valid option.")
except ValueError:
print("Invalid input. Please enter a number for your selection.")
selected_trade = results[choice - 1]
trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050005', 'records':[{'type': '0', 'value': selected_trade['trade']['id']}]})
astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(trade_data)))
return await task
def decrypt_secret(stub, decoded_trade):
invoice, auth, payload = decoded_trade['secret']
decoded_invoice = stub.DecodePayReq(ln.PayReqString(pay_req=invoice))
print('Invoice for secret decoded.')
print('Destination:', decoded_invoice.destination)
print('Amount:', decoded_invoice.num_satoshis)
print('Description:', decoded_invoice.description)
ask_pay = input('Pay the invoice and decrypt the secret? [y/N]: ')
if ask_pay.lower() == 'y':
routerstub = lnrouter.RouterStub(lnd_connect())
for response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(payment_request=invoice, timeout_seconds=60)):
if response.status == 2:
print('Payment paid!')
preimage = bytes.fromhex(response.payment_preimage)
if response.status > 2:
print('Payment failed. Please try again.')
return
signerstub = lnsigner.SignerStub(lnd_connect())
shared_key = signerstub.DeriveSharedKey(lns.SharedKeyRequest(ephemeral_pubkey=bytes.fromhex(decoded_invoice.destination))).shared_key
shared_secret = bytes(x ^ y for x, y in zip(shared_key, preimage))
cipher = Cipher(algorithms.AES(shared_secret), modes.GCM(bytes(16), bytes.fromhex(auth)), backend=default_backend())
decryptor = cipher.decryptor()
decrypted = decryptor.update(bytes.fromhex(payload)) + decryptor.finalize()
print('Successfully decrypted the secret:', decrypted.decode('utf-8'))
def main():
options = ["Buy A Trade", "Setup A Sale", "Serve Trades"]
print("Select an option:")
for idx, option in enumerate(options, start=1):
print(f"{idx}. {option}")
while True:
try:
choice = int(input("Enter the number of your choice: "))
if 1 <= choice <= len(options):
break
else:
print("Invalid input. Please enter a valid option.")
except ValueError:
print("Invalid input. Please enter a number for your selection.")
selected_option = options[choice - 1]
stub = lnrpc.LightningStub(lnd_connect())
if selected_option == 'Setup A Sale':
description = input('Enter a description for the trade: ')
price = input('Enter the price to charge in sats: ')
expiry = input('Enter desired days until trade expiry: ')
secret = input('Enter the secret you want to sell: ')
create_trade_anchor(stub, description, price, secret, expiry)
ask_serve = input('Start serving trades? y/N: ')
if ask_serve.lower() == 'y':
print('Generic Trade Link:', create_trade_details(stub))
serve_trades(stub)
if selected_option == 'Serve Trades':
print('Generic Trade Link:', create_trade_details(stub))
serve_trades(stub)
if selected_option == 'Buy A Trade':
trade = input('Enter an encoded trade: ')
decoded_trade = decode_trade_data(trade)
if 'secret' in decoded_trade:
decrypt_secret(stub, decoded_trade)
if 'connect' in decoded_trade:
network, id, connection = decoded_trade['connect']
if 'node' in connection[0]:
try:
to_peer = connection[0]['node']['id']
if not (Peers.objects.filter(pubkey=to_peer).exists() and Peers.objects.filter(pubkey=to_peer)[0].connected == True):
host = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=to_peer, include_channels=False)).node.addresses[0].addr
stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=to_peer, host=host), timeout=60))
except:
raise ValueError('PeerConnectionError')
else:
raise ValueError('NoPeerFoundInConnectionData')
trade = asyncio.run(request_trades(to_peer))
if trade:
trade_data = next((record for record in trade if record['type'] == '1'), None)
if trade_data:
decoded_trade = decode_trade_data(trade_data['value'])
decrypt_secret(stub, decoded_trade)
if __name__ == '__main__':
main()