mirror of
https://github.com/aljazceru/payments-rest-api.git
synced 2025-12-21 15:34:22 +01:00
11
README.md
11
README.md
@@ -1 +1,10 @@
|
|||||||
# nodeless-payments
|
# Nodeless payments
|
||||||
|
Proof of concept implementation for deploying nodeless sdk as lambda function to AWS. This gives us a REST api which close to zero cost.
|
||||||
|
|
||||||
|
Seed phrase and breez api key are stored encrypted in AWS Parameter store and decrypted when lamba accessed (a rest call is made).
|
||||||
|
|
||||||
|
Currently implemented endpoints
|
||||||
|
- /send_payment (bolt11)
|
||||||
|
- /receive_payment (bolt11)
|
||||||
|
- /list_payments
|
||||||
|
|
||||||
|
|||||||
46
deploy.sh
Normal file
46
deploy.sh
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
FUNCTION_NAME="nodeless-payments"
|
||||||
|
ROLE_ARN="arn:aws:iam::<ARN>:role/lambda-breez-role"
|
||||||
|
REGION="<region>"
|
||||||
|
HANDLER="lambda_function.lambda_handler"
|
||||||
|
RUNTIME="python3.12"
|
||||||
|
ZIP_FILE="lambda_package.zip"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
mkdir -p package
|
||||||
|
pip install -r requirements.txt -t package/
|
||||||
|
|
||||||
|
# Package the function
|
||||||
|
echo "Packaging the function..."
|
||||||
|
cp lambda_function.py package/
|
||||||
|
cd package
|
||||||
|
zip -r ../$ZIP_FILE .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Check if function exists
|
||||||
|
EXISTS=$(aws lambda get-function --function-name $FUNCTION_NAME --region $REGION --query 'Configuration.FunctionArn' --output text 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$EXISTS" ]; then
|
||||||
|
echo "Creating new Lambda function..."
|
||||||
|
aws lambda create-function \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--runtime $RUNTIME \
|
||||||
|
--role $ROLE_ARN \
|
||||||
|
--handler $HANDLER \
|
||||||
|
--timeout 30 \
|
||||||
|
--memory-size 256 \
|
||||||
|
--region $REGION \
|
||||||
|
--zip-file fileb://$ZIP_FILE
|
||||||
|
else
|
||||||
|
echo "Updating existing Lambda function..."
|
||||||
|
aws lambda update-function-code \
|
||||||
|
--function-name $FUNCTION_NAME \
|
||||||
|
--region $REGION \
|
||||||
|
--zip-file fileb://$ZIP_FILE
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment complete!"
|
||||||
|
|
||||||
246
lambda_function.py
Normal file
246
lambda_function.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
from typing import Optional
|
||||||
|
from breez_sdk_liquid import (
|
||||||
|
LiquidNetwork,
|
||||||
|
PayAmount,
|
||||||
|
ConnectRequest,
|
||||||
|
PrepareSendRequest,
|
||||||
|
SendPaymentRequest,
|
||||||
|
PrepareReceiveRequest,
|
||||||
|
ReceivePaymentRequest,
|
||||||
|
EventListener,
|
||||||
|
SdkEvent,
|
||||||
|
connect,
|
||||||
|
default_config,
|
||||||
|
PaymentMethod,
|
||||||
|
ListPaymentsRequest
|
||||||
|
)
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from aws_lambda_powertools import Logger, Tracer
|
||||||
|
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
|
||||||
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
|
||||||
|
logger = Logger()
|
||||||
|
tracer = Tracer()
|
||||||
|
app = APIGatewayRestResolver()
|
||||||
|
|
||||||
|
class SdkListener(EventListener):
|
||||||
|
def __init__(self):
|
||||||
|
self.synced = False
|
||||||
|
self.paid = []
|
||||||
|
|
||||||
|
def on_event(self, event):
|
||||||
|
if isinstance(event, SdkEvent.SYNCED):
|
||||||
|
self.synced = True
|
||||||
|
if isinstance(event, SdkEvent.PAYMENT_SUCCEEDED):
|
||||||
|
if event.details.destination:
|
||||||
|
self.paid.append(event.details.destination)
|
||||||
|
|
||||||
|
def is_paid(self, destination: str):
|
||||||
|
return destination in self.paid
|
||||||
|
|
||||||
|
class PaymentHandler:
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = self._get_ssm_parameter('/breez/api_key')
|
||||||
|
self.seed_phrase = self._get_ssm_parameter('/breez/seed_phrase')
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
raise Exception("Missing Breez API key in Parameter Store")
|
||||||
|
if not self.seed_phrase:
|
||||||
|
raise Exception("Missing seed phrase in Parameter Store")
|
||||||
|
|
||||||
|
logger.info("Retrieved encrypted parameters successfully")
|
||||||
|
|
||||||
|
config = default_config(LiquidNetwork.MAINNET, self.api_key)
|
||||||
|
config.working_dir = '/tmp'
|
||||||
|
connect_request = ConnectRequest(config=config, mnemonic=self.seed_phrase)
|
||||||
|
self.instance = connect(connect_request)
|
||||||
|
self.listener = SdkListener()
|
||||||
|
self.instance.add_event_listener(self.listener)
|
||||||
|
|
||||||
|
def _get_ssm_parameter(self, param_name: str) -> str:
|
||||||
|
"""Get an encrypted parameter from AWS Systems Manager Parameter Store"""
|
||||||
|
logger.info(f"Retrieving encrypted parameter: {param_name}")
|
||||||
|
ssm = boto3.client('ssm')
|
||||||
|
try:
|
||||||
|
response = ssm.get_parameter(
|
||||||
|
Name=param_name,
|
||||||
|
WithDecryption=True
|
||||||
|
)
|
||||||
|
return response['Parameter']['Value']
|
||||||
|
except ssm.exceptions.ParameterNotFound:
|
||||||
|
logger.error(f"Parameter {param_name} not found in Parameter Store")
|
||||||
|
raise Exception(f"Parameter {param_name} not found in Parameter Store")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get parameter {param_name}: {str(e)}", exc_info=True)
|
||||||
|
raise Exception(f"Failed to get parameter {param_name}: {str(e)}")
|
||||||
|
|
||||||
|
def wait_for_sync(self, timeout_seconds: int = 30):
|
||||||
|
"""Wait for the SDK to sync before proceeding."""
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout_seconds:
|
||||||
|
if self.listener.synced:
|
||||||
|
return True
|
||||||
|
time.sleep(1)
|
||||||
|
raise Exception("Sync timeout: SDK did not sync within the allocated time.")
|
||||||
|
|
||||||
|
def wait_for_payment(self, destination: str, timeout_seconds: int = 60) -> bool:
|
||||||
|
"""Wait for payment to complete or timeout"""
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout_seconds:
|
||||||
|
if self.listener.is_paid(destination):
|
||||||
|
return True
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_payments(self, params: dict = None) -> dict:
|
||||||
|
try:
|
||||||
|
self.wait_for_sync() # Ensure sync before executing
|
||||||
|
from_ts = int(params.get('from_timestamp')) if params and params.get('from_timestamp') is not None else None
|
||||||
|
to_ts = int(params.get('to_timestamp')) if params and params.get('to_timestamp') is not None else None
|
||||||
|
offset = int(params.get('offset')) if params and params.get('offset') is not None else None
|
||||||
|
limit = int(params.get('limit')) if params and params.get('limit') is not None else None
|
||||||
|
|
||||||
|
req = ListPaymentsRequest(
|
||||||
|
from_timestamp=from_ts,
|
||||||
|
to_timestamp=to_ts,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
payments = self.instance.list_payments(req)
|
||||||
|
payment_list = []
|
||||||
|
for payment in payments:
|
||||||
|
payment_dict = {
|
||||||
|
'timestamp': payment.timestamp,
|
||||||
|
'amount_sat': payment.amount_sat,
|
||||||
|
'fees_sat': payment.fees_sat,
|
||||||
|
'payment_type': str(payment.payment_type),
|
||||||
|
'status': str(payment.status),
|
||||||
|
'details': str(payment.details),
|
||||||
|
'destination': payment.destination,
|
||||||
|
'tx_id': payment.tx_id
|
||||||
|
}
|
||||||
|
payment_list.append(payment_dict)
|
||||||
|
|
||||||
|
# apply offset, limit and timestamp filters
|
||||||
|
print(f"payment_list: {payment_list}")
|
||||||
|
filtered_payments = []
|
||||||
|
for payment in payment_list:
|
||||||
|
if from_ts and payment['timestamp'] < from_ts:
|
||||||
|
continue
|
||||||
|
if to_ts and payment['timestamp'] > to_ts:
|
||||||
|
continue
|
||||||
|
if offset and offset > 0:
|
||||||
|
offset -= 1
|
||||||
|
continue
|
||||||
|
filtered_payments.append(payment)
|
||||||
|
|
||||||
|
# apply limit
|
||||||
|
if limit and limit < len(filtered_payments):
|
||||||
|
filtered_payments = filtered_payments[:limit]
|
||||||
|
|
||||||
|
# apply offset
|
||||||
|
if offset and offset > 0:
|
||||||
|
filtered_payments = payment_list[offset:]
|
||||||
|
|
||||||
|
if not (offset or limit or from_ts or to_ts):
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': json.dumps({
|
||||||
|
'payments': payment_list
|
||||||
|
})
|
||||||
|
}
|
||||||
|
# apply limit
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': json.dumps({
|
||||||
|
'payments': filtered_payments
|
||||||
|
})
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'statusCode': 500,
|
||||||
|
'body': json.dumps({'error': str(e)})
|
||||||
|
}
|
||||||
|
|
||||||
|
def receive_payment(self, amount: int, payment_method: str = 'LIGHTNING') -> dict:
|
||||||
|
try:
|
||||||
|
self.wait_for_sync() # Ensure sync before executing
|
||||||
|
prepare_req = PrepareReceiveRequest(getattr(PaymentMethod, payment_method), amount)
|
||||||
|
prepare_res = self.instance.prepare_receive_payment(prepare_req)
|
||||||
|
req = ReceivePaymentRequest(prepare_res)
|
||||||
|
res = self.instance.receive_payment(req)
|
||||||
|
|
||||||
|
# Return the invoice details immediately
|
||||||
|
return {
|
||||||
|
'statusCode': 200,
|
||||||
|
'body': json.dumps({
|
||||||
|
'destination': res.destination,
|
||||||
|
'fees_sat': prepare_res.fees_sat
|
||||||
|
})
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}
|
||||||
|
|
||||||
|
def send_payment(self, destination: str, amount: Optional[int] = None, drain: bool = False) -> dict:
|
||||||
|
try:
|
||||||
|
self.wait_for_sync() # Ensure sync before executing
|
||||||
|
pay_amount = PayAmount.DRAIN if drain else PayAmount.RECEIVER(amount) if amount else None
|
||||||
|
prepare_req = PrepareSendRequest(destination, pay_amount)
|
||||||
|
prepare_res = self.instance.prepare_send_payment(prepare_req)
|
||||||
|
req = SendPaymentRequest(prepare_res)
|
||||||
|
res = self.instance.send_payment(req)
|
||||||
|
return {'statusCode': 200, 'body': json.dumps({'payment_status': 'success', 'destination': res.payment.destination, 'fees_sat': prepare_res.fees_sat})}
|
||||||
|
except Exception as e:
|
||||||
|
return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}
|
||||||
|
|
||||||
|
@app.get("/list_payments")
|
||||||
|
@tracer.capture_method
|
||||||
|
def list_payments():
|
||||||
|
try:
|
||||||
|
logger.info("Processing list_payments request")
|
||||||
|
handler = PaymentHandler()
|
||||||
|
return handler.list_payments(app.current_event.query_string_parameters or {})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing payments: {str(e)}", exc_info=True)
|
||||||
|
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||||
|
|
||||||
|
@app.post("/receive_payment")
|
||||||
|
@tracer.capture_method
|
||||||
|
def receive_payment():
|
||||||
|
try:
|
||||||
|
body = app.current_event.json_body
|
||||||
|
logger.info(f"Processing receive_payment request with body: {body}")
|
||||||
|
|
||||||
|
handler = PaymentHandler()
|
||||||
|
return handler.receive_payment(
|
||||||
|
amount=body['amount'],
|
||||||
|
payment_method=body.get('method', 'LIGHTNING')
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error receiving payment: {str(e)}", exc_info=True)
|
||||||
|
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||||
|
|
||||||
|
@app.post("/send_payment")
|
||||||
|
@tracer.capture_method
|
||||||
|
def send_payment():
|
||||||
|
try:
|
||||||
|
body = app.current_event.json_body
|
||||||
|
logger.info(f"Processing send_payment request with body: {body}")
|
||||||
|
|
||||||
|
handler = PaymentHandler()
|
||||||
|
return handler.send_payment(
|
||||||
|
destination=body['destination'],
|
||||||
|
amount=body.get('amount'),
|
||||||
|
drain=body.get('drain', False)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending payment: {str(e)}", exc_info=True)
|
||||||
|
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||||
|
|
||||||
|
@logger.inject_lambda_context
|
||||||
|
@tracer.capture_lambda_handler
|
||||||
|
def lambda_handler(event: dict, context: LambdaContext) -> dict:
|
||||||
|
return app.resolve(event, context)
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
aws-lambda-powertools
|
||||||
|
breez-sdk-liquid==0.6.3
|
||||||
|
boto3
|
||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
|
aws-xray-sdk
|
||||||
Reference in New Issue
Block a user