Skip to content

Commit 2ca5990

Browse files
authored
Merge pull request #128 from tnull/2023-06-add-python-packaging
Add python packaging
2 parents 6cf15c4 + 67dbc56 commit 2ca5990

File tree

7 files changed

+323
-3
lines changed

7 files changed

+323
-3
lines changed

.github/workflows/python.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Continuous Integration Checks - Python
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
check-python:
7+
runs-on: ubuntu-latest
8+
9+
env:
10+
LDK_NODE_PYTHON_DIR: bindings/python
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Generate Python bindings
22+
run: ./scripts/uniffi_bindgen_generate_python.sh
23+
24+
- name: Start bitcoind and electrs
25+
run: docker compose up -d
26+
27+
- name: Install testing prerequisites
28+
run: |
29+
pip3 install requests
30+
31+
- name: Run Python unit tests
32+
env:
33+
BITCOIN_CLI_BIN: "docker exec ldk-node-bitcoin-1 bitcoin-cli"
34+
BITCOIND_RPC_USER: "user"
35+
BITCOIND_RPC_PASSWORD: "pass"
36+
ESPLORA_ENDPOINT: "http://127.0.0.1:3002"
37+
run: |
38+
cd $LDK_NODE_PYTHON_DIR
39+
python3 -m unittest discover -s src/ldk_node

bindings/python/pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "ldk_node"
3+
version = "0.1-alpha.1"
4+
authors = [
5+
{ name="Elias Rohrer", email="[email protected]" },
6+
]
7+
description = "A ready-to-go Lightning node library built using LDK and BDK."
8+
readme = "README.md"
9+
requires-python = ">=3.6"
10+
classifiers = [
11+
"Topic :: Software Development :: Libraries",
12+
"Topic :: Security :: Cryptography",
13+
"License :: OSI Approved :: MIT License",
14+
"License :: OSI Approved :: Apache Software License",
15+
"Programming Language :: Python :: 3",
16+
]
17+
18+
[project.urls]
19+
"Homepage" = "https://lightningdevkit.org/"
20+
"Github" = "https://github.com/lightningdevkit/ldk-node"
21+
"Bug Tracker" = "https://github.com/lightningdevkit/ldk-node/issues"

bindings/python/setup.cfg

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[options]
2+
packages = find:
3+
package_dir =
4+
= src
5+
include_package_data = True
6+
7+
[options.packages.find]
8+
where = src
9+
10+
[options.package_data]
11+
ldk_node =
12+
*.so
13+
*.dylib
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ldk_node.ldk_node import *
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import unittest
2+
import tempfile
3+
import time
4+
import subprocess
5+
import os
6+
import re
7+
import requests
8+
9+
from ldk_node import *
10+
11+
DEFAULT_ESPLORA_SERVER_URL = "http://127.0.0.1:3002"
12+
DEFAULT_TEST_NETWORK = Network.REGTEST
13+
DEFAULT_BITCOIN_CLI_BIN = "bitcoin-cli"
14+
15+
def bitcoin_cli(cmd):
16+
args = []
17+
18+
bitcoin_cli_bin = [DEFAULT_BITCOIN_CLI_BIN]
19+
if os.environ.get('BITCOIN_CLI_BIN'):
20+
bitcoin_cli_bin = os.environ['BITCOIN_CLI_BIN'].split()
21+
22+
args += bitcoin_cli_bin
23+
args += ["-regtest"]
24+
25+
if os.environ.get('BITCOIND_RPC_USER'):
26+
args += ["-rpcuser=" + os.environ['BITCOIND_RPC_USER']]
27+
28+
if os.environ.get('BITCOIND_RPC_PASSWORD'):
29+
args += ["-rpcpassword=" + os.environ['BITCOIND_RPC_PASSWORD']]
30+
31+
for c in cmd.split():
32+
args += [c]
33+
34+
print("RUNNING:", args)
35+
res = subprocess.run(args, capture_output=True)
36+
return str(res.stdout.decode("utf-8"))
37+
38+
def mine(blocks):
39+
address = bitcoin_cli("getnewaddress").strip()
40+
mining_res = bitcoin_cli("generatetoaddress " + str(blocks) + " " + str(address))
41+
print("MINING_RES:", mining_res)
42+
43+
m = re.search("\\n.+\n\\]$", mining_res)
44+
last_block = str(m.group(0))
45+
return str(last_block.strip().replace('"','').replace('\n]',''))
46+
47+
def mine_and_wait(esplora_endpoint, blocks):
48+
last_block = mine(blocks)
49+
wait_for_block(esplora_endpoint, last_block)
50+
51+
def wait_for_block(esplora_endpoint, block_hash):
52+
url = esplora_endpoint + "/block/" + block_hash + "/status"
53+
esplora_picked_up_block = False
54+
while not esplora_picked_up_block:
55+
res = requests.get(url)
56+
try:
57+
json = res.json()
58+
esplora_picked_up_block = json['in_best_chain']
59+
except:
60+
pass
61+
time.sleep(1)
62+
63+
def wait_for_tx(esplora_endpoint, txid):
64+
url = esplora_endpoint + "/tx/" + txid
65+
esplora_picked_up_tx = False
66+
while not esplora_picked_up_tx:
67+
res = requests.get(url)
68+
try:
69+
json = res.json()
70+
esplora_picked_up_tx = json['txid'] == txid
71+
except:
72+
pass
73+
time.sleep(1)
74+
75+
def send_to_address(address, amount_sats):
76+
amount_btc = amount_sats/100000000.0
77+
cmd = "sendtoaddress " + str(address) + " " + str(amount_btc)
78+
res = str(bitcoin_cli(cmd)).strip()
79+
print("SEND TX:", res)
80+
return res
81+
82+
83+
def setup_node(tmp_dir, esplora_endpoint, listening_address):
84+
config = Config()
85+
builder = Builder.from_config(config)
86+
builder.set_storage_dir_path(tmp_dir)
87+
builder.set_esplora_server(esplora_endpoint)
88+
builder.set_network(DEFAULT_TEST_NETWORK)
89+
builder.set_listening_address(listening_address)
90+
return builder.build()
91+
92+
def get_esplora_endpoint():
93+
if os.environ.get('ESPLORA_ENDPOINT'):
94+
return str(os.environ['ESPLORA_ENDPOINT'])
95+
return DEFAULT_ESPLORA_SERVER_URL
96+
97+
class TestLdkNode(unittest.TestCase):
98+
def setUp(self):
99+
bitcoin_cli("createwallet ldk_node_test")
100+
mine(101)
101+
time.sleep(3)
102+
esplora_endpoint = get_esplora_endpoint()
103+
mine_and_wait(esplora_endpoint, 1)
104+
105+
def test_channel_full_cycle(self):
106+
esplora_endpoint = get_esplora_endpoint()
107+
108+
## Setup Node 1
109+
tmp_dir_1 = tempfile.TemporaryDirectory("_ldk_node_1")
110+
print("TMP DIR 1:", tmp_dir_1.name)
111+
112+
listening_address_1 = "127.0.0.1:2323"
113+
node_1 = setup_node(tmp_dir_1.name, esplora_endpoint, listening_address_1)
114+
node_1.start()
115+
node_id_1 = node_1.node_id()
116+
print("Node ID 1:", node_id_1)
117+
118+
# Setup Node 2
119+
tmp_dir_2 = tempfile.TemporaryDirectory("_ldk_node_2")
120+
print("TMP DIR 2:", tmp_dir_2.name)
121+
122+
listening_address_2 = "127.0.0.1:2324"
123+
node_2 = setup_node(tmp_dir_2.name, esplora_endpoint, listening_address_2)
124+
node_2.start()
125+
node_id_2 = node_2.node_id()
126+
print("Node ID 2:", node_id_2)
127+
128+
address_1 = node_1.new_onchain_address()
129+
txid_1 = send_to_address(address_1, 100000)
130+
address_2 = node_2.new_onchain_address()
131+
txid_2 = send_to_address(address_2, 100000)
132+
133+
wait_for_tx(esplora_endpoint, txid_1)
134+
wait_for_tx(esplora_endpoint, txid_2)
135+
136+
mine_and_wait(esplora_endpoint, 6)
137+
138+
node_1.sync_wallets()
139+
node_2.sync_wallets()
140+
141+
spendable_balance_1 = node_1.spendable_onchain_balance_sats()
142+
spendable_balance_2 = node_2.spendable_onchain_balance_sats()
143+
total_balance_1 = node_1.total_onchain_balance_sats()
144+
total_balance_2 = node_2.total_onchain_balance_sats()
145+
146+
print("SPENDABLE 1:", spendable_balance_1)
147+
self.assertEqual(spendable_balance_1, 100000)
148+
149+
print("SPENDABLE 2:", spendable_balance_2)
150+
self.assertEqual(spendable_balance_2, 100000)
151+
152+
print("TOTAL 1:", total_balance_1)
153+
self.assertEqual(total_balance_1, 100000)
154+
155+
print("TOTAL 2:", total_balance_2)
156+
self.assertEqual(total_balance_2, 100000)
157+
158+
node_1.connect_open_channel(node_id_2, listening_address_2, 50000, None, None, True)
159+
160+
channel_pending_event_1 = node_1.wait_next_event()
161+
assert isinstance(channel_pending_event_1, Event.CHANNEL_PENDING)
162+
print("EVENT:", channel_pending_event_1)
163+
node_1.event_handled()
164+
165+
channel_pending_event_2 = node_2.wait_next_event()
166+
assert isinstance(channel_pending_event_2, Event.CHANNEL_PENDING)
167+
print("EVENT:", channel_pending_event_2)
168+
node_2.event_handled()
169+
170+
funding_txid = channel_pending_event_1.funding_txo.txid
171+
wait_for_tx(esplora_endpoint, funding_txid)
172+
mine_and_wait(esplora_endpoint, 6)
173+
174+
node_1.sync_wallets()
175+
node_2.sync_wallets()
176+
177+
channel_ready_event_1 = node_1.wait_next_event()
178+
assert isinstance(channel_ready_event_1, Event.CHANNEL_READY)
179+
print("EVENT:", channel_ready_event_1)
180+
print("funding_txo:", funding_txid)
181+
node_1.event_handled()
182+
183+
channel_ready_event_2 = node_2.wait_next_event()
184+
assert isinstance(channel_ready_event_2, Event.CHANNEL_READY)
185+
print("EVENT:", channel_ready_event_2)
186+
node_2.event_handled()
187+
188+
invoice = node_2.receive_payment(2500000, "asdf", 9217)
189+
node_1.send_payment(invoice)
190+
191+
payment_successful_event_1 = node_1.wait_next_event()
192+
assert isinstance(payment_successful_event_1, Event.PAYMENT_SUCCESSFUL)
193+
print("EVENT:", payment_successful_event_1)
194+
node_1.event_handled()
195+
196+
payment_received_event_2 = node_2.wait_next_event()
197+
assert isinstance(payment_received_event_2, Event.PAYMENT_RECEIVED)
198+
print("EVENT:", payment_received_event_2)
199+
node_2.event_handled()
200+
201+
node_2.close_channel(channel_ready_event_2.channel_id, node_id_1)
202+
203+
channel_closed_event_1 = node_1.wait_next_event()
204+
assert isinstance(channel_closed_event_1, Event.CHANNEL_CLOSED)
205+
print("EVENT:", channel_closed_event_1)
206+
node_1.event_handled()
207+
208+
channel_closed_event_2 = node_2.wait_next_event()
209+
assert isinstance(channel_closed_event_2, Event.CHANNEL_CLOSED)
210+
print("EVENT:", channel_closed_event_2)
211+
node_2.event_handled()
212+
213+
mine_and_wait(esplora_endpoint, 1)
214+
215+
node_1.sync_wallets()
216+
node_2.sync_wallets()
217+
218+
spendable_balance_after_close_1 = node_1.spendable_onchain_balance_sats()
219+
assert spendable_balance_after_close_1 > 95000
220+
assert spendable_balance_after_close_1 < 100000
221+
spendable_balance_after_close_2 = node_2.spendable_onchain_balance_sats()
222+
self.assertEqual(spendable_balance_after_close_2, 102500)
223+
224+
# Stop nodes
225+
node_1.stop()
226+
node_2.stop()
227+
228+
# Cleanup
229+
time.sleep(1) # Wait a sec so our logs can finish writing
230+
tmp_dir_1.cleanup()
231+
tmp_dir_2.cleanup()
232+
233+
if __name__ == '__main__':
234+
unittest.main()
235+

scripts/python_create_package.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
cd bindings/python || exit 1
3+
python3 -m build
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
#!/bin/bash
2-
BINDINGS_DIR="./bindings/python"
2+
BINDINGS_DIR="./bindings/python/src/ldk_node"
33
UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml"
44

5-
cargo build --release --features uniffi || exit 1
5+
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
6+
DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.so"
7+
else
8+
DYNAMIC_LIB_PATH="./target/release-smaller/libldk_node.dylib"
9+
fi
10+
11+
cargo build --profile release-smaller --features uniffi || exit 1
612
$UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language python -o "$BINDINGS_DIR" || exit 1
7-
cp ./target/release/libldk_node.dylib "$BINDINGS_DIR"/libldk_node.dylib || exit 1
13+
14+
mkdir -p $BINDINGS_DIR
15+
cp "$DYNAMIC_LIB_PATH" "$BINDINGS_DIR" || exit 1

0 commit comments

Comments
 (0)