2
.gitignore
vendored
@@ -128,6 +128,8 @@ celerybeat.pid
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
*/.env
|
||||
*/.venv
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
|
||||
155
README.md
@@ -1,156 +1,11 @@
|
||||
# Nodeless payments
|
||||
This is a proof of concept implementation for deploying the Breez SDK (Nodeless implementation) as a lambda function to AWS. It provides a REST api with close to zero cost of hosting.
|
||||
|
||||
# Breez Nodeless SDK
|
||||
Experimental deployments of Breez Nodeless SDK to various cloud platforms as REST API.
|
||||
|
||||
Currently implemented endpoints:
|
||||
- /send_payment (bolt11)
|
||||
- /receive_payment (bolt11)
|
||||
- /list_payments
|
||||
|
||||
|
||||
### API Key Security
|
||||
- X-API-KEY header serves as authorization method for accessing the API. Anyone that knows the API url and API_SECRET can access your funds, so make sure to protect this secret and to generate a unique and long string. You can use generators like [this](https://1password.com/password-generator) or [this](https://www.uuidgenerator.net/).
|
||||
- Encrypted secrets are stored in [AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) and are accessed each time any endpoint is called (in the background docker container is started for each REST API call).
|
||||
|
||||
## Requirements for deployment
|
||||
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
|
||||
- [Access to AWS account](https://signin.aws.amazon.com/signup?request_type=register)
|
||||
- [Breez SDK - Nodeless implementation API key](https://breez.technology/request-api-key/#contact-us-form-sdk)
|
||||
- 12 words BIP 39 seed (TBA: use Misty Breez to generate it)
|
||||
|
||||
## Deployment
|
||||
Deployment to AWS with [cloudformation](./cloudformation.yaml).
|
||||
|
||||
### Install CLI
|
||||
Follow [AWS guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) to install it on your computer.
|
||||
|
||||
### Create credentials
|
||||
There are several ways of creating credentials to deploy in AWS. Ideally, you want to generate temporary credentials that are gonna be revoked after this deployment. You can create create credentials that have the same permissions as your root account. This will enable you to run all the CLI commands. Follow these steps to create an access key:
|
||||
|
||||
* Select *Security Credentials* from your account's menu:
|
||||
<img src="./docs/screenshot0.jpg" width="50%">
|
||||
<img src="./docs/screenshot1.jpg" width="30%">
|
||||
|
||||
* Follow the steps to create an access key:
|
||||
<img src="./docs/screenshot2.jpg" width="50%">
|
||||
<img src="./docs/screenshot3.jpg" width="50%">
|
||||
<img src="./docs/screenshot4.jpg" width="50%">
|
||||
|
||||
### Configure CLI
|
||||
Now that you have AWS CLI installed and credentials ready, it's time for the last step of the requirements: configuring the AWS CLI to work with your account credentials.
|
||||
|
||||
You will also have to choose a default region where you want to deploy your API. You can see the list of all regions [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). You should pick the region that is closest to your business. For quick reference:
|
||||
* **US**: *us-east-1*, *us-west-1*
|
||||
* **Europe**: *eu-central-1*, *eu-west-1*
|
||||
* **LATAM**: *sa-east-1*
|
||||
* **Asia**: *ap-southeast-1*
|
||||
|
||||
Once you have an API key, an API secret and region string, you're ready to configure the CLI.
|
||||
|
||||
Open a command line interface in your OS and type `aws configure` and press enter. Now, copy/paste the API key, press enter, then copy/paste the API secret and press enter. Do the same for the region string. You can leave the default output format blank.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```
|
||||
# aws configure
|
||||
AWS Access Key ID [None]: AKIA44HIGHQYZHRTZ7WP
|
||||
AWS Secret Access Key [None]: qKVd5nMA7y8DbEuvF6kFbKTcYrAow8rH9KDxWGkT
|
||||
Default region name [None]: us-east-1
|
||||
Default output format [None]:
|
||||
```
|
||||
|
||||
### Create SSM parameters for Breez credentials
|
||||
From the command line, run the following commands:
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/api_key" --value "<REPLACE_WITH_BREEZ_API_KEY>" --type SecureString
|
||||
```
|
||||
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/seed_phrase" --value "<REPLACE_WITH_SEED_WORDS>" --type SecureString
|
||||
```
|
||||
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/api_secret" --value "<REPLACE_WITH_DESIRED_API_AUTHENTICATION_KEY>" --type SecureString
|
||||
```
|
||||
### Deploy Cloudformation stack
|
||||
* Download this configuration file: [cloudformation.yaml](https://raw.githubusercontent.com/breez/nodeless-payments/refs/heads/main/cloudformation.yaml).
|
||||
* Deploy the stack:
|
||||
```
|
||||
aws cloudformation create-stack --stack-name breez-integration --template-body file://cloudformation.yaml --capabilities CAPABILITY_IAM
|
||||
```
|
||||
* Monitor the stack creation (wait until it changes to *CREATE_COMPLETE*):
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query Stacks[0].StackStatus
|
||||
```
|
||||
* Retrieve the API endpoints:
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query 'Stacks[0].Outputs'
|
||||
```
|
||||
Output should look like this:
|
||||
```
|
||||
root@2edec8635e65:/# aws cloudformation describe-stacks --stack-name breez-integration --query 'Stacks[0].Outputs'
|
||||
[
|
||||
{
|
||||
"OutputKey": "ApiGatewayBaseURL",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod",
|
||||
"Description": "Base URL for API Gateway"
|
||||
},
|
||||
{
|
||||
"OutputKey": "SendEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/send_payment",
|
||||
"Description": "Send endpoint URL"
|
||||
},
|
||||
{
|
||||
"OutputKey": "PaymentsEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/list_payments",
|
||||
"Description": "Payments endpoint URL"
|
||||
},
|
||||
{
|
||||
"OutputKey": "ReceiveEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/receive_payment",
|
||||
"Description": "Receive endpoint URL"
|
||||
}
|
||||
]
|
||||
```
|
||||
* If the deployment was successful, you should deactivate your API key now.
|
||||
### Example usage
|
||||
#### Python
|
||||
You can use `example-client.py`file from this to test the functionality. Take Base URL from the output of last command (see *ApiGatewayBaseURL* example above) and API_SECRET and edit the `example-client.py` with correct values
|
||||
|
||||
```
|
||||
API_URL = "YOUR-URL-HERE"
|
||||
API_KEY = "YOUR-SECRET-HERE"
|
||||
```
|
||||
For example-client to work, you need to have python installed together with requests library:
|
||||
```
|
||||
pip install requests
|
||||
```
|
||||
Then run:
|
||||
```
|
||||
python example-client.py
|
||||
```
|
||||
#### curl
|
||||
If you don't have python installed, you can also just run a curl command.
|
||||
|
||||
For example, for the *list_payments* endpoint, run:
|
||||
```
|
||||
curl -X POST "<YOUR-URL-HERE>/list_payments" -H "Content-Type: application/json" -H "x-api-key: <API_SECRET>" -d '{}'
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
To remove the stack you deployed you need to run the command delete-stack. This command starts the process to delete the stack, but it takes a while.
|
||||
```
|
||||
aws cloudformation delete-stack --stack-name breez-integration
|
||||
```
|
||||
You can use the same status command to see if its been successfully deleted:
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query Stacks[0].StackStatus
|
||||
```
|
||||
|
||||
You should also cleanup the parameters:
|
||||
```
|
||||
aws ssm delete-parameter --name "/breez-nodeless/api_key"
|
||||
aws ssm delete-parameter --name "/breez-nodeless/seed_phrase"
|
||||
aws ssm delete-parameter --name "/breez-nodeless/api_secret"
|
||||
|
||||
```
|
||||
## Options
|
||||
- [fly.io](./fly/README.md) - deploy fly.io app
|
||||
- [aws lambda](./lambda/README.md) - lambda functions (wip)
|
||||
6
fly/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Breez SDK configuration
|
||||
BREEZ_API_KEY=your_breez_api_key_here
|
||||
SEED_PHRASE=your_mnemonic_seed_phrase_here
|
||||
|
||||
# API security
|
||||
API_SECRET=your_api_secret_here
|
||||
37
fly/DEV.md
Normal file
@@ -0,0 +1,37 @@
|
||||
### Local Development
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/breez/nodeless-payments.git
|
||||
cd nodeless-payments/fly
|
||||
```
|
||||
|
||||
2. Install dependencies with Poetry:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Create a `.env` file with your configuration:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env file with your actual credentials
|
||||
```
|
||||
|
||||
4. Run the application:
|
||||
```bash
|
||||
poetry run uvicorn main:app --reload
|
||||
```
|
||||
|
||||
5. Visit `http://localhost:8000/docs` in your browser to see the API documentation
|
||||
|
||||
### Docker
|
||||
|
||||
1. Build the Docker image:
|
||||
```bash
|
||||
docker build -t breez-nodeless .
|
||||
```
|
||||
|
||||
2. Run the container:
|
||||
```bash
|
||||
docker run -p 8000:8000 --env-file .env breez-nodeless
|
||||
```
|
||||
49
fly/Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies and Python 3.12
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
curl \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-pip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set python and pip alternatives
|
||||
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 && \
|
||||
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1
|
||||
|
||||
# Install Poetry
|
||||
RUN pip install poetry --break-system-packages
|
||||
|
||||
# Copy project files
|
||||
COPY pyproject.toml .
|
||||
COPY main.py .
|
||||
|
||||
# Create a README.md file if it doesn't exist to satisfy Poetry
|
||||
RUN touch README.md
|
||||
|
||||
# Copy environment file template
|
||||
COPY .env.example .env
|
||||
|
||||
# Configure Poetry to not create a virtual environment
|
||||
RUN poetry config virtualenvs.create false
|
||||
|
||||
# Install dependencies without installing the project itself
|
||||
RUN poetry install --no-interaction --no-ansi --no-root
|
||||
|
||||
# Create tmp directory for Breez SDK
|
||||
RUN mkdir -p ./tmp
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 8000
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
129
fly/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Breez Nodeless FastAPI
|
||||
|
||||
A FastAPI implementation of the Breez Nodeless SDK. This service provides a REST API for sending and receiving payments via the Lightning Network running on fly.io.
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Poetry (package manager)
|
||||
- Breez Nodeless SDK API key (get one from [Breez](https://breez.technology/))
|
||||
- A valid seed phrase for the Breez SDK wallet
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
## Deployment to Fly.io
|
||||
|
||||
1. Install the Fly CLI:
|
||||
```bash
|
||||
# macOS
|
||||
brew install flyctl
|
||||
|
||||
# Linux
|
||||
curl -L https://fly.io/install.sh | sh
|
||||
|
||||
# Windows
|
||||
pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
|
||||
```
|
||||
|
||||
2. Log in to Fly:
|
||||
```bash
|
||||
fly auth login
|
||||
```
|
||||
|
||||
3. Launch the app:
|
||||
```bash
|
||||
fly launch
|
||||
```
|
||||
|
||||
4. Set secrets:
|
||||
```bash
|
||||
fly secrets set BREEZ_API_KEY=your_breez_api_key
|
||||
fly secrets set SEED_PHRASE=your_mnemonic_seed_phrase
|
||||
fly secrets set API_SECRET=your_api_secret
|
||||
```
|
||||
|
||||
5. Deploy the app:
|
||||
```bash
|
||||
fly deploy
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Health Check
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
Check if the API is up and running.
|
||||
|
||||
### List Payments
|
||||
|
||||
```
|
||||
GET /list_payments
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
- `from_timestamp` (optional): Filter payments from this timestamp
|
||||
- `to_timestamp` (optional): Filter payments to this timestamp
|
||||
- `offset` (optional): Pagination offset
|
||||
- `limit` (optional): Pagination limit
|
||||
|
||||
### Receive Payment
|
||||
|
||||
```
|
||||
POST /receive_payment
|
||||
```
|
||||
|
||||
Request Body:
|
||||
```json
|
||||
{
|
||||
"amount": 10000,
|
||||
"method": "LIGHTNING"
|
||||
}
|
||||
```
|
||||
|
||||
### Send Payment
|
||||
|
||||
```
|
||||
POST /send_payment
|
||||
```
|
||||
|
||||
Request Body:
|
||||
```json
|
||||
{
|
||||
"destination": "lnbc...",
|
||||
"amount": 10000,
|
||||
"drain": false
|
||||
}
|
||||
```
|
||||
|
||||
## Client Usage
|
||||
|
||||
See `client.py` for a Python client implementation and example usage.
|
||||
|
||||
### Example usage
|
||||
#### Python
|
||||
You can use `example-client.py`file from this to test the functionality. Take the URL flyctl returned at deploy and API_SECRET and edit the `example-client.py` with correct values
|
||||
|
||||
```
|
||||
API_URL = "YOUR-URL-HERE"
|
||||
API_KEY = "YOUR-SECRET-HERE"
|
||||
```
|
||||
For example-client to work, you need to have python installed together with requests library:
|
||||
```
|
||||
pip install requests
|
||||
```
|
||||
Then run:
|
||||
```
|
||||
python example-client.py
|
||||
```
|
||||
#### curl
|
||||
If you don't have python installed, you can also just run a curl command.
|
||||
|
||||
For example, for the *list_payments* endpoint, run:
|
||||
```
|
||||
curl -X POST "<YOUR-URL-HERE>/list_payments" -H "Content-Type: application/json" -H "x-api-key: <API_SECRET>" -d '{}'
|
||||
```
|
||||
148
fly/example_client.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
class BreezClient:
|
||||
def __init__(self, api_url, api_key):
|
||||
"""
|
||||
Initialize the Breez client.
|
||||
|
||||
Args:
|
||||
api_url (str): The base URL of the Breez API
|
||||
api_key (str): The API key for authentication
|
||||
"""
|
||||
self.api_url = api_url
|
||||
self.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': api_key
|
||||
}
|
||||
|
||||
def list_payments(self, from_timestamp=None, to_timestamp=None, offset=None, limit=None):
|
||||
"""
|
||||
List all payments with optional filters.
|
||||
|
||||
Args:
|
||||
from_timestamp (int, optional): Filter payments from this timestamp
|
||||
to_timestamp (int, optional): Filter payments to this timestamp
|
||||
offset (int, optional): Pagination offset
|
||||
limit (int, optional): Pagination limit
|
||||
|
||||
Returns:
|
||||
dict: JSON response with payment list
|
||||
"""
|
||||
params = {}
|
||||
if from_timestamp is not None:
|
||||
params["from_timestamp"] = from_timestamp
|
||||
if to_timestamp is not None:
|
||||
params["to_timestamp"] = to_timestamp
|
||||
if offset is not None:
|
||||
params["offset"] = offset
|
||||
if limit is not None:
|
||||
params["limit"] = limit
|
||||
|
||||
response = requests.get(
|
||||
f"{self.api_url}/list_payments",
|
||||
params=params,
|
||||
headers=self.headers
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def receive_payment(self, amount, method="LIGHTNING"):
|
||||
"""
|
||||
Generate a Lightning/Bitcoin/Liquid invoice to receive payment.
|
||||
|
||||
Args:
|
||||
amount (int): Amount in satoshis to receive
|
||||
method (str, optional): Payment method (LIGHTNING or LIQUID)
|
||||
|
||||
Returns:
|
||||
dict: JSON response with invoice details
|
||||
"""
|
||||
payload = {
|
||||
"amount": amount,
|
||||
"method": method
|
||||
}
|
||||
response = requests.post(
|
||||
f"{self.api_url}/receive_payment",
|
||||
json=payload,
|
||||
headers=self.headers
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def send_payment(self, destination, amount=None, drain=False):
|
||||
"""
|
||||
Send a payment via Lightning or Liquid.
|
||||
|
||||
Args:
|
||||
destination (str): Payment destination (invoice or address)
|
||||
amount (int, optional): Amount in satoshis to send
|
||||
drain (bool, optional): Whether to drain the wallet
|
||||
|
||||
Returns:
|
||||
dict: JSON response with payment details
|
||||
"""
|
||||
payload = {
|
||||
"destination": destination
|
||||
}
|
||||
if amount is not None:
|
||||
payload["amount"] = amount
|
||||
if drain:
|
||||
payload["drain"] = True
|
||||
|
||||
response = requests.post(
|
||||
f"{self.api_url}/send_payment",
|
||||
json=payload,
|
||||
headers=self.headers
|
||||
)
|
||||
return self._handle_response(response)
|
||||
|
||||
def health_check(self):
|
||||
"""
|
||||
Check if the API is healthy and responding.
|
||||
|
||||
Returns:
|
||||
dict: JSON response with health status
|
||||
"""
|
||||
response = requests.get(f"{self.api_url}/health")
|
||||
return self._handle_response(response)
|
||||
|
||||
def _handle_response(self, response):
|
||||
"""Helper method to handle API responses."""
|
||||
try:
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return {
|
||||
"error": f"Request failed with status {response.status_code}",
|
||||
"details": response.text
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to process response: {str(e)}"}
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
# Configuration
|
||||
API_URL = "http://localhost:8000" # Change to your deployed API URL
|
||||
API_KEY = "" # Set your API key here
|
||||
|
||||
# Initialize client
|
||||
breez = BreezClient(api_url=API_URL, api_key=API_KEY)
|
||||
|
||||
# Check API health
|
||||
print("🔍 Checking API health...")
|
||||
print(breez.health_check())
|
||||
|
||||
# List payments
|
||||
print("\n🔄 Listing Payments...")
|
||||
print(json.dumps(breez.list_payments(), indent=2))
|
||||
|
||||
# Generate an invoice to receive payment
|
||||
#print("\n💰 Generating invoice to receive payment...")
|
||||
#invoice = breez.receive_payment(amount=1000, method="LIGHTNING")
|
||||
#print(json.dumps(invoice, indent=2))
|
||||
#print(f"Invoice: {invoice.get('destination', 'Error generating invoice')}")
|
||||
|
||||
# Send payment example (commented out for safety)
|
||||
#print("\n🚀 Sending Payment...")
|
||||
#result = breez.send_payment(destination="", amount=1111)
|
||||
#print(json.dumps(result, indent=2))
|
||||
37
fly/fly.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
# fly.toml app configuration file generated for breez-nodeless-api on 2025-04-29T18:26:24+02:00
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = 'breez-nodeless-api'
|
||||
primary_region = 'ewr'
|
||||
|
||||
[build]
|
||||
dockerfile = 'Dockerfile'
|
||||
|
||||
[env]
|
||||
PORT = '8000'
|
||||
|
||||
[[mounts]]
|
||||
source = 'breez_data'
|
||||
destination = '/app/tmp'
|
||||
|
||||
[http_service]
|
||||
internal_port = 8000
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 1
|
||||
processes = ['app']
|
||||
|
||||
[http_service.concurrency]
|
||||
type = 'connections'
|
||||
hard_limit = 1000
|
||||
soft_limit = 500
|
||||
|
||||
[http_service.http_options]
|
||||
|
||||
[[vm]]
|
||||
memory = '1gb'
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
289
fly/main.py
Normal file
@@ -0,0 +1,289 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, Header, Query
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, List, Any, Union
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from enum import Enum
|
||||
from breez_sdk_liquid import (
|
||||
LiquidNetwork,
|
||||
PayAmount,
|
||||
ConnectRequest,
|
||||
PrepareSendRequest,
|
||||
SendPaymentRequest,
|
||||
PrepareReceiveRequest,
|
||||
ReceivePaymentRequest,
|
||||
EventListener,
|
||||
SdkEvent,
|
||||
connect,
|
||||
default_config,
|
||||
PaymentMethod,
|
||||
ListPaymentsRequest,
|
||||
ReceiveAmount
|
||||
)
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Breez Nodeless Payments API",
|
||||
description="A FastAPI implementation of Breez SDK for Lightning/Liquid payments",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# API Key authentication
|
||||
API_KEY_NAME = "x-api-key"
|
||||
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
||||
|
||||
# Configure API key from environment variable
|
||||
API_KEY = os.getenv("API_SECRET")
|
||||
BREEZ_API_KEY = os.getenv("BREEZ_API_KEY")
|
||||
SEED_PHRASE = os.getenv("SEED_PHRASE")
|
||||
|
||||
# Models for request/response
|
||||
class PaymentMethodEnum(str, Enum):
|
||||
LIGHTNING = "LIGHTNING"
|
||||
LIQUID = "LIQUID"
|
||||
|
||||
class ReceivePaymentBody(BaseModel):
|
||||
amount: int = Field(..., description="Amount in satoshis to receive")
|
||||
method: PaymentMethodEnum = Field(PaymentMethodEnum.LIGHTNING, description="Payment method")
|
||||
|
||||
class SendPaymentBody(BaseModel):
|
||||
destination: str = Field(..., description="Payment destination (invoice or address)")
|
||||
amount: Optional[int] = Field(None, description="Amount in satoshis to send")
|
||||
drain: bool = Field(False, description="Whether to drain the wallet")
|
||||
|
||||
class ListPaymentsParams:
|
||||
def __init__(
|
||||
self,
|
||||
from_timestamp: Optional[int] = Query(None, description="Filter payments from this timestamp"),
|
||||
to_timestamp: Optional[int] = Query(None, description="Filter payments to this timestamp"),
|
||||
offset: Optional[int] = Query(None, description="Pagination offset"),
|
||||
limit: Optional[int] = Query(None, description="Pagination limit")
|
||||
):
|
||||
self.from_timestamp = from_timestamp
|
||||
self.to_timestamp = to_timestamp
|
||||
self.offset = offset
|
||||
self.limit = limit
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
timestamp: int
|
||||
amount_sat: int
|
||||
fees_sat: int
|
||||
payment_type: str
|
||||
status: str
|
||||
details: str
|
||||
destination: str
|
||||
tx_id: Optional[str] = None
|
||||
|
||||
class PaymentListResponse(BaseModel):
|
||||
payments: List[PaymentResponse]
|
||||
|
||||
class ReceiveResponse(BaseModel):
|
||||
destination: str
|
||||
fees_sat: int
|
||||
|
||||
class SendResponse(BaseModel):
|
||||
payment_status: str
|
||||
destination: str
|
||||
fees_sat: int
|
||||
|
||||
# Breez SDK Event Listener
|
||||
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
|
||||
|
||||
# Initialize Breez SDK client
|
||||
class BreezClient:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BreezClient, cls).__new__(cls)
|
||||
cls._instance.initialize()
|
||||
return cls._instance
|
||||
|
||||
def initialize(self):
|
||||
if not BREEZ_API_KEY:
|
||||
raise Exception("Missing Breez API key in environment variables")
|
||||
if not SEED_PHRASE:
|
||||
raise Exception("Missing seed phrase in environment variables")
|
||||
|
||||
config = default_config(LiquidNetwork.MAINNET, BREEZ_API_KEY)
|
||||
config.working_dir = './tmp'
|
||||
connect_request = ConnectRequest(config=config, mnemonic=SEED_PHRASE)
|
||||
self.instance = connect(connect_request)
|
||||
self.listener = SdkListener()
|
||||
self.instance.add_event_listener(self.listener)
|
||||
self.is_initialized = True
|
||||
|
||||
def wait_for_sync(self, timeout_seconds: int = 30):
|
||||
"""Wait for the SDK to sync before proceeding."""
|
||||
import time
|
||||
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 list_payments(self, params: ListPaymentsParams) -> List[Dict[str, Any]]:
|
||||
self.wait_for_sync()
|
||||
|
||||
req = ListPaymentsRequest(
|
||||
from_timestamp=params.from_timestamp,
|
||||
to_timestamp=params.to_timestamp,
|
||||
offset=params.offset,
|
||||
limit=params.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)
|
||||
|
||||
return payment_list
|
||||
|
||||
def receive_payment(self, amount: int, payment_method: str = 'LIGHTNING') -> Dict[str, Any]:
|
||||
try:
|
||||
self.wait_for_sync()
|
||||
except Exception as e:
|
||||
raise Exception(f"Error during SDK sync: {e}")
|
||||
|
||||
try:
|
||||
if isinstance(amount, int):
|
||||
receive_amount = ReceiveAmount.BITCOIN(amount)
|
||||
else:
|
||||
receive_amount = amount
|
||||
prepare_req = PrepareReceiveRequest(payment_method=getattr(PaymentMethod, payment_method), amount=receive_amount)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error preparing receive request: {e}")
|
||||
|
||||
try:
|
||||
prepare_res = self.instance.prepare_receive_payment(prepare_req)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error preparing receive payment: {e}")
|
||||
|
||||
try:
|
||||
req = ReceivePaymentRequest(prepare_response=prepare_res)
|
||||
res = self.instance.receive_payment(req)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error receiving payment: {e}")
|
||||
|
||||
return {
|
||||
'destination': res.destination,
|
||||
'fees_sat': prepare_res.fees_sat
|
||||
}
|
||||
|
||||
def send_payment(self, destination: str, amount: Optional[int] = None, drain: bool = False) -> Dict[str, Any]:
|
||||
self.wait_for_sync()
|
||||
|
||||
pay_amount = PayAmount.DRAIN if drain else PayAmount.BITCOIN(amount) if amount else None
|
||||
prepare_req = PrepareSendRequest(destination=destination, amount=pay_amount)
|
||||
prepare_res = self.instance.prepare_send_payment(prepare_req)
|
||||
req = SendPaymentRequest(prepare_response=prepare_res)
|
||||
res = self.instance.send_payment(req)
|
||||
|
||||
return {
|
||||
'payment_status': 'success',
|
||||
'destination': res.payment.destination,
|
||||
'fees_sat': prepare_res.fees_sat
|
||||
}
|
||||
|
||||
# Dependency for API key validation
|
||||
async def get_api_key(api_key: str = Header(None, alias=API_KEY_NAME)):
|
||||
if not API_KEY:
|
||||
raise HTTPException(status_code=500, detail="API key not configured on server")
|
||||
|
||||
if api_key != API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API Key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
return api_key
|
||||
|
||||
# Dependency for Breez client
|
||||
def get_breez_client():
|
||||
try:
|
||||
return BreezClient()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to initialize Breez client: {str(e)}")
|
||||
|
||||
# API Routes
|
||||
@app.get("/list_payments", response_model=PaymentListResponse)
|
||||
async def list_payments(
|
||||
params: ListPaymentsParams = Depends(),
|
||||
api_key: str = Depends(get_api_key),
|
||||
client: BreezClient = Depends(get_breez_client)
|
||||
):
|
||||
try:
|
||||
payments = client.list_payments(params)
|
||||
return {"payments": payments}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/receive_payment", response_model=ReceiveResponse)
|
||||
async def receive_payment(
|
||||
request: ReceivePaymentBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
client: BreezClient = Depends(get_breez_client)
|
||||
):
|
||||
try:
|
||||
result = client.receive_payment(
|
||||
amount=request.amount,
|
||||
payment_method=request.method
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/send_payment", response_model=SendResponse)
|
||||
async def send_payment(
|
||||
request: SendPaymentBody,
|
||||
api_key: str = Depends(get_api_key),
|
||||
client: BreezClient = Depends(get_breez_client)
|
||||
):
|
||||
try:
|
||||
result = client.send_payment(
|
||||
destination=request.destination,
|
||||
amount=request.amount,
|
||||
drain=request.drain
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
19
fly/pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[tool.poetry]
|
||||
name = "breez-fastapi"
|
||||
version = "0.1.0"
|
||||
description = "A FastAPI implementation of Breez SDK for Lightning/Liquid payments"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
fastapi = "^0.111.0"
|
||||
uvicorn = {extras = ["standard"], version = "^0.30.1"}
|
||||
breez-sdk-liquid = "*"
|
||||
python-dotenv = "^1.0.1"
|
||||
requests = "^2.31.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
156
lambda/README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Nodeless payments
|
||||
This is a proof of concept implementation for deploying the Breez SDK (Nodeless implementation) as a lambda function to AWS. It provides a REST api with close to zero cost of hosting.
|
||||
|
||||
|
||||
Currently implemented endpoints:
|
||||
- /send_payment (bolt11)
|
||||
- /receive_payment (bolt11)
|
||||
- /list_payments
|
||||
|
||||
|
||||
### API Key Security
|
||||
- X-API-KEY header serves as authorization method for accessing the API. Anyone that knows the API url and API_SECRET can access your funds, so make sure to protect this secret and to generate a unique and long string. You can use generators like [this](https://1password.com/password-generator) or [this](https://www.uuidgenerator.net/).
|
||||
- Encrypted secrets are stored in [AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) and are accessed each time any endpoint is called (in the background docker container is started for each REST API call).
|
||||
|
||||
## Requirements for deployment
|
||||
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
|
||||
- [Access to AWS account](https://signin.aws.amazon.com/signup?request_type=register)
|
||||
- [Breez SDK - Nodeless implementation API key](https://breez.technology/request-api-key/#contact-us-form-sdk)
|
||||
- 12 words BIP 39 seed (TBA: use Misty Breez to generate it)
|
||||
|
||||
## Deployment
|
||||
Deployment to AWS with [cloudformation](./cloudformation.yaml).
|
||||
|
||||
### Install CLI
|
||||
Follow [AWS guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) to install it on your computer.
|
||||
|
||||
### Create credentials
|
||||
There are several ways of creating credentials to deploy in AWS. Ideally, you want to generate temporary credentials that are gonna be revoked after this deployment. You can create create credentials that have the same permissions as your root account. This will enable you to run all the CLI commands. Follow these steps to create an access key:
|
||||
|
||||
* Select *Security Credentials* from your account's menu:
|
||||
<img src="./docs/screenshot0.jpg" width="50%">
|
||||
<img src="./docs/screenshot1.jpg" width="30%">
|
||||
|
||||
* Follow the steps to create an access key:
|
||||
<img src="./docs/screenshot2.jpg" width="50%">
|
||||
<img src="./docs/screenshot3.jpg" width="50%">
|
||||
<img src="./docs/screenshot4.jpg" width="50%">
|
||||
|
||||
### Configure CLI
|
||||
Now that you have AWS CLI installed and credentials ready, it's time for the last step of the requirements: configuring the AWS CLI to work with your account credentials.
|
||||
|
||||
You will also have to choose a default region where you want to deploy your API. You can see the list of all regions [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). You should pick the region that is closest to your business. For quick reference:
|
||||
* **US**: *us-east-1*, *us-west-1*
|
||||
* **Europe**: *eu-central-1*, *eu-west-1*
|
||||
* **LATAM**: *sa-east-1*
|
||||
* **Asia**: *ap-southeast-1*
|
||||
|
||||
Once you have an API key, an API secret and region string, you're ready to configure the CLI.
|
||||
|
||||
Open a command line interface in your OS and type `aws configure` and press enter. Now, copy/paste the API key, press enter, then copy/paste the API secret and press enter. Do the same for the region string. You can leave the default output format blank.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```
|
||||
# aws configure
|
||||
AWS Access Key ID [None]: AKIA44HIGHQYZHRTZ7WP
|
||||
AWS Secret Access Key [None]: qKVd5nMA7y8DbEuvF6kFbKTcYrAow8rH9KDxWGkT
|
||||
Default region name [None]: us-east-1
|
||||
Default output format [None]:
|
||||
```
|
||||
|
||||
### Create SSM parameters for Breez credentials
|
||||
From the command line, run the following commands:
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/api_key" --value "<REPLACE_WITH_BREEZ_API_KEY>" --type SecureString
|
||||
```
|
||||
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/seed_phrase" --value "<REPLACE_WITH_SEED_WORDS>" --type SecureString
|
||||
```
|
||||
|
||||
```
|
||||
aws ssm put-parameter --name "/breez-nodeless/api_secret" --value "<REPLACE_WITH_DESIRED_API_AUTHENTICATION_KEY>" --type SecureString
|
||||
```
|
||||
### Deploy Cloudformation stack
|
||||
* Download this configuration file: [cloudformation.yaml](https://raw.githubusercontent.com/breez/nodeless-payments/refs/heads/main/cloudformation.yaml).
|
||||
* Deploy the stack:
|
||||
```
|
||||
aws cloudformation create-stack --stack-name breez-integration --template-body file://cloudformation.yaml --capabilities CAPABILITY_IAM
|
||||
```
|
||||
* Monitor the stack creation (wait until it changes to *CREATE_COMPLETE*):
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query Stacks[0].StackStatus
|
||||
```
|
||||
* Retrieve the API endpoints:
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query 'Stacks[0].Outputs'
|
||||
```
|
||||
Output should look like this:
|
||||
```
|
||||
root@2edec8635e65:/# aws cloudformation describe-stacks --stack-name breez-integration --query 'Stacks[0].Outputs'
|
||||
[
|
||||
{
|
||||
"OutputKey": "ApiGatewayBaseURL",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod",
|
||||
"Description": "Base URL for API Gateway"
|
||||
},
|
||||
{
|
||||
"OutputKey": "SendEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/send_payment",
|
||||
"Description": "Send endpoint URL"
|
||||
},
|
||||
{
|
||||
"OutputKey": "PaymentsEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/list_payments",
|
||||
"Description": "Payments endpoint URL"
|
||||
},
|
||||
{
|
||||
"OutputKey": "ReceiveEndpoint",
|
||||
"OutputValue": "https://yxzjorems5.execute-api.us-east-1.amazonaws.com/prod/receive_payment",
|
||||
"Description": "Receive endpoint URL"
|
||||
}
|
||||
]
|
||||
```
|
||||
* If the deployment was successful, you should deactivate your API key now.
|
||||
### Example usage
|
||||
#### Python
|
||||
You can use `example-client.py`file from this to test the functionality. Take Base URL from the output of last command (see *ApiGatewayBaseURL* example above) and API_SECRET and edit the `example-client.py` with correct values
|
||||
|
||||
```
|
||||
API_URL = "YOUR-URL-HERE"
|
||||
API_KEY = "YOUR-SECRET-HERE"
|
||||
```
|
||||
For example-client to work, you need to have python installed together with requests library:
|
||||
```
|
||||
pip install requests
|
||||
```
|
||||
Then run:
|
||||
```
|
||||
python example-client.py
|
||||
```
|
||||
#### curl
|
||||
If you don't have python installed, you can also just run a curl command.
|
||||
|
||||
For example, for the *list_payments* endpoint, run:
|
||||
```
|
||||
curl -X POST "<YOUR-URL-HERE>/list_payments" -H "Content-Type: application/json" -H "x-api-key: <API_SECRET>" -d '{}'
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
To remove the stack you deployed you need to run the command delete-stack. This command starts the process to delete the stack, but it takes a while.
|
||||
```
|
||||
aws cloudformation delete-stack --stack-name breez-integration
|
||||
```
|
||||
You can use the same status command to see if its been successfully deleted:
|
||||
```
|
||||
aws cloudformation describe-stacks --stack-name breez-integration --query Stacks[0].StackStatus
|
||||
```
|
||||
|
||||
You should also cleanup the parameters:
|
||||
```
|
||||
aws ssm delete-parameter --name "/breez-nodeless/api_key"
|
||||
aws ssm delete-parameter --name "/breez-nodeless/seed_phrase"
|
||||
aws ssm delete-parameter --name "/breez-nodeless/api_secret"
|
||||
|
||||
```
|
||||
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |