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