Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add python packaging #128

Merged
merged 3 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Continuous Integration Checks - Python

on: [push, pull_request]

jobs:
check-python:
runs-on: ubuntu-latest

env:
LDK_NODE_PYTHON_DIR: bindings/python

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Generate Python bindings
run: ./scripts/uniffi_bindgen_generate_python.sh

- name: Start bitcoind and electrs
run: docker compose up -d

- name: Install testing prerequisites
run: |
pip3 install requests

- name: Run Python unit tests
env:
BITCOIN_CLI_BIN: "docker exec ldk-node-bitcoin-1 bitcoin-cli"
BITCOIND_RPC_USER: "user"
BITCOIND_RPC_PASSWORD: "pass"
ESPLORA_ENDPOINT: "http://127.0.0.1:3002"
run: |
cd $LDK_NODE_PYTHON_DIR
python3 -m unittest discover -s src/ldk_node
21 changes: 21 additions & 0 deletions bindings/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[project]
name = "ldk_node"
version = "0.1-alpha.1"
authors = [
{ name="Elias Rohrer", email="[email protected]" },
]
description = "A ready-to-go Lightning node library built using LDK and BDK."
readme = "README.md"
requires-python = ">=3.6"
classifiers = [
"Topic :: Software Development :: Libraries",
"Topic :: Security :: Cryptography",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
]

[project.urls]
"Homepage" = "https://lightningdevkit.org/"
"Github" = "https://github.com/lightningdevkit/ldk-node"
"Bug Tracker" = "https://github.com/lightningdevkit/ldk-node/issues"
13 changes: 13 additions & 0 deletions bindings/python/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[options]
packages = find:
package_dir =
= src
include_package_data = True

[options.packages.find]
where = src

[options.package_data]
ldk_node =
*.so
*.dylib
1 change: 1 addition & 0 deletions bindings/python/src/ldk_node/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ldk_node.ldk_node import *
235 changes: 235 additions & 0 deletions bindings/python/src/ldk_node/test_ldk_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import unittest
import tempfile
import time
import subprocess
import os
import re
import requests

from ldk_node import *

DEFAULT_ESPLORA_SERVER_URL = "http://127.0.0.1:3002"
DEFAULT_TEST_NETWORK = Network.REGTEST
DEFAULT_BITCOIN_CLI_BIN = "bitcoin-cli"

def bitcoin_cli(cmd):
args = []

bitcoin_cli_bin = [DEFAULT_BITCOIN_CLI_BIN]
if os.environ.get('BITCOIN_CLI_BIN'):
bitcoin_cli_bin = os.environ['BITCOIN_CLI_BIN'].split()

args += bitcoin_cli_bin
args += ["-regtest"]

if os.environ.get('BITCOIND_RPC_USER'):
args += ["-rpcuser=" + os.environ['BITCOIND_RPC_USER']]

if os.environ.get('BITCOIND_RPC_PASSWORD'):
args += ["-rpcpassword=" + os.environ['BITCOIND_RPC_PASSWORD']]

for c in cmd.split():
args += [c]

print("RUNNING:", args)
res = subprocess.run(args, capture_output=True)
return str(res.stdout.decode("utf-8"))

def mine(blocks):
address = bitcoin_cli("getnewaddress").strip()
mining_res = bitcoin_cli("generatetoaddress " + str(blocks) + " " + str(address))
print("MINING_RES:", mining_res)

m = re.search("\\n.+\n\\]$", mining_res)
last_block = str(m.group(0))
return str(last_block.strip().replace('"','').replace('\n]',''))

def mine_and_wait(esplora_endpoint, blocks):
last_block = mine(blocks)
wait_for_block(esplora_endpoint, last_block)

def wait_for_block(esplora_endpoint, block_hash):
url = esplora_endpoint + "/block/" + block_hash + "/status"
esplora_picked_up_block = False
while not esplora_picked_up_block:
res = requests.get(url)
try:
json = res.json()
esplora_picked_up_block = json['in_best_chain']
except:
pass
time.sleep(1)

def wait_for_tx(esplora_endpoint, txid):
url = esplora_endpoint + "/tx/" + txid
esplora_picked_up_tx = False
while not esplora_picked_up_tx:
res = requests.get(url)
try:
json = res.json()
esplora_picked_up_tx = json['txid'] == txid
except:
pass
time.sleep(1)

def send_to_address(address, amount_sats):
amount_btc = amount_sats/100000000.0
cmd = "sendtoaddress " + str(address) + " " + str(amount_btc)
res = str(bitcoin_cli(cmd)).strip()
print("SEND TX:", res)
return res


def setup_node(tmp_dir, esplora_endpoint, listening_address):
config = Config()
builder = Builder.from_config(config)
builder.set_storage_dir_path(tmp_dir)
builder.set_esplora_server(esplora_endpoint)
builder.set_network(DEFAULT_TEST_NETWORK)
builder.set_listening_address(listening_address)
return builder.build()

def get_esplora_endpoint():
if os.environ.get('ESPLORA_ENDPOINT'):
return str(os.environ['ESPLORA_ENDPOINT'])
return DEFAULT_ESPLORA_SERVER_URL

class TestLdkNode(unittest.TestCase):
def setUp(self):
bitcoin_cli("createwallet ldk_node_test")
mine(101)
time.sleep(3)
esplora_endpoint = get_esplora_endpoint()
mine_and_wait(esplora_endpoint, 1)

def test_channel_full_cycle(self):
esplora_endpoint = get_esplora_endpoint()

## Setup Node 1
tmp_dir_1 = tempfile.TemporaryDirectory("_ldk_node_1")
print("TMP DIR 1:", tmp_dir_1.name)

listening_address_1 = "127.0.0.1:2323"
node_1 = setup_node(tmp_dir_1.name, esplora_endpoint, listening_address_1)
node_1.start()
node_id_1 = node_1.node_id()
print("Node ID 1:", node_id_1)

# Setup Node 2
tmp_dir_2 = tempfile.TemporaryDirectory("_ldk_node_2")
print("TMP DIR 2:", tmp_dir_2.name)

listening_address_2 = "127.0.0.1:2324"
node_2 = setup_node(tmp_dir_2.name, esplora_endpoint, listening_address_2)
node_2.start()
node_id_2 = node_2.node_id()
print("Node ID 2:", node_id_2)

address_1 = node_1.new_onchain_address()
txid_1 = send_to_address(address_1, 100000)
address_2 = node_2.new_onchain_address()
txid_2 = send_to_address(address_2, 100000)

wait_for_tx(esplora_endpoint, txid_1)
wait_for_tx(esplora_endpoint, txid_2)

mine_and_wait(esplora_endpoint, 6)

node_1.sync_wallets()
node_2.sync_wallets()

spendable_balance_1 = node_1.spendable_onchain_balance_sats()
spendable_balance_2 = node_2.spendable_onchain_balance_sats()
total_balance_1 = node_1.total_onchain_balance_sats()
total_balance_2 = node_2.total_onchain_balance_sats()

print("SPENDABLE 1:", spendable_balance_1)
self.assertEqual(spendable_balance_1, 100000)

print("SPENDABLE 2:", spendable_balance_2)
self.assertEqual(spendable_balance_2, 100000)

print("TOTAL 1:", total_balance_1)
self.assertEqual(total_balance_1, 100000)

print("TOTAL 2:", total_balance_2)
self.assertEqual(total_balance_2, 100000)

node_1.connect_open_channel(node_id_2, listening_address_2, 50000, None, None, True)

channel_pending_event_1 = node_1.wait_next_event()
assert isinstance(channel_pending_event_1, Event.CHANNEL_PENDING)
print("EVENT:", channel_pending_event_1)
node_1.event_handled()

channel_pending_event_2 = node_2.wait_next_event()
assert isinstance(channel_pending_event_2, Event.CHANNEL_PENDING)
print("EVENT:", channel_pending_event_2)
node_2.event_handled()

funding_txid = channel_pending_event_1.funding_txo.txid
wait_for_tx(esplora_endpoint, funding_txid)
mine_and_wait(esplora_endpoint, 6)

node_1.sync_wallets()
node_2.sync_wallets()

channel_ready_event_1 = node_1.wait_next_event()
assert isinstance(channel_ready_event_1, Event.CHANNEL_READY)
print("EVENT:", channel_ready_event_1)
print("funding_txo:", funding_txid)
node_1.event_handled()

channel_ready_event_2 = node_2.wait_next_event()
assert isinstance(channel_ready_event_2, Event.CHANNEL_READY)
print("EVENT:", channel_ready_event_2)
node_2.event_handled()

invoice = node_2.receive_payment(2500000, "asdf", 9217)
node_1.send_payment(invoice)

payment_successful_event_1 = node_1.wait_next_event()
assert isinstance(payment_successful_event_1, Event.PAYMENT_SUCCESSFUL)
print("EVENT:", payment_successful_event_1)
node_1.event_handled()

payment_received_event_2 = node_2.wait_next_event()
assert isinstance(payment_received_event_2, Event.PAYMENT_RECEIVED)
print("EVENT:", payment_received_event_2)
node_2.event_handled()

node_2.close_channel(channel_ready_event_2.channel_id, node_id_1)

channel_closed_event_1 = node_1.wait_next_event()
assert isinstance(channel_closed_event_1, Event.CHANNEL_CLOSED)
print("EVENT:", channel_closed_event_1)
node_1.event_handled()

channel_closed_event_2 = node_2.wait_next_event()
assert isinstance(channel_closed_event_2, Event.CHANNEL_CLOSED)
print("EVENT:", channel_closed_event_2)
node_2.event_handled()

mine_and_wait(esplora_endpoint, 1)

node_1.sync_wallets()
node_2.sync_wallets()

spendable_balance_after_close_1 = node_1.spendable_onchain_balance_sats()
assert spendable_balance_after_close_1 > 95000
assert spendable_balance_after_close_1 < 100000
spendable_balance_after_close_2 = node_2.spendable_onchain_balance_sats()
self.assertEqual(spendable_balance_after_close_2, 102500)

# Stop nodes
node_1.stop()
node_2.stop()

# Cleanup
time.sleep(1) # Wait a sec so our logs can finish writing
tmp_dir_1.cleanup()
tmp_dir_2.cleanup()

if __name__ == '__main__':
unittest.main()

3 changes: 3 additions & 0 deletions scripts/python_create_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
cd bindings/python || exit 1
python3 -m build
14 changes: 11 additions & 3 deletions scripts/uniffi_bindgen_generate_python.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
#!/bin/bash
BINDINGS_DIR="./bindings/python"
BINDINGS_DIR="./bindings/python/src/ldk_node"
UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml"

cargo build --release --features uniffi || exit 1
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.so"
else
DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.dylib"
fi

cargo build --profile release-smaller --features uniffi || exit 1
$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language python -o "$BINDINGS_DIR" || exit 1
cp ./target/release/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1

mkdir -p $BINDINGS_DIR
cp "$DYNAMIC_LIB_PATH" "$BINDINGS_DIR" || exit 1