Add tests for onchain address, pgp sign verification. Improve Dockerfile

This commit is contained in:
Reckless_Satoshi 2023-11-15 19:48:04 +00:00 committed by Reckless_Satoshi
parent 79a3df66a2
commit 3e0d451e97
17 changed files with 335 additions and 132 deletions

View File

@ -32,7 +32,6 @@ jobs:
- name: Patch Dockerfile and .env-sample
run: |
sed -i "1s/FROM python:.*/FROM python:${{ matrix.python-tag }}/" Dockerfile
sed -i '/RUN pip install --no-cache-dir -r requirements.txt/a COPY requirements_dev.txt .\nRUN pip install --no-cache-dir -r requirements_dev.txt' Dockerfile
sed -i "s/^LNVENDOR=.*/LNVENDOR='${{ matrix.ln-vendor }}'/" .env-sample
- uses: satackey/action-docker-layer-caching@v0.0.11

View File

@ -1,5 +1,6 @@
FROM python:3.11.6-slim-bookworm
ARG DEBIAN_FRONTEND=noninteractive
ARG DEVELOPMENT=False
RUN mkdir -p /usr/src/robosats
WORKDIR /usr/src/robosats
@ -17,6 +18,11 @@ RUN python -m pip install --upgrade pip
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY requirements_dev.txt ./
RUN if [ "$DEVELOPMENT" = "true" ]; then \
pip install --no-cache-dir -r requirements_dev.txt; \
fi
# copy current dir's content to container's WORKDIR root i.e. all the contents of the robosats app
COPY . .

View File

@ -79,6 +79,14 @@ class CLNNode:
except Exception as e:
print(f"Cannot get CLN node id: {e}")
@classmethod
def newaddress(cls):
"""Only used on tests to fund the regtest node"""
nodestub = node_pb2_grpc.NodeStub(cls.node_channel)
request = node_pb2.NewaddrRequest()
response = nodestub.NewAddr(request)
return response.bech32
@classmethod
def decode_payreq(cls, invoice):
"""Decodes a lightning payment request (invoice)"""

View File

@ -105,10 +105,11 @@ class LNDNode:
lightningstub = lightning_pb2_grpc.LightningStub(cls.channel)
request = lightning_pb2.GetInfoRequest()
response = lightningstub.GetInfo(request)
log("lightning_pb2_grpc.GetInfo", request, response)
if response.testnet:
dummy_address = "tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x"
elif response.chains[0].network == "regtest":
dummy_address = "bcrt1q3w8xja7knmycsglnxg2xzjq8uv9u7jdwau25nl"
else:
dummy_address = "bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3"
# We assume segwit. Use hardcoded address as shortcut so there is no need of user inputs yet.

View File

@ -168,8 +168,8 @@ class TestUtils(TestCase):
def test_validate_pgp_keys(self):
# Example test client generated GPG keys
client_pub_key = r"-----BEGIN PGP PUBLIC KEY BLOCK-----\\xjMEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0\mUR0SKqLmdjNTFJvYm9TYXRzIElEIDU1MmRkMWE2NjFhN2FjYTRhNDFmODg5\MTBmZjM0YWMzYjFhYzgwYmI3Nzk0ZWQ5ZmQ1NWQ4Yjc2Yjk3YWFkOTfCjAQQ\FgoAPgWCZTWJ1wQLCQcICZA3N7au4gi/zgMVCAoEFgACAQIZAQKbAwIeARYh\BO5iBLnj0J/E6sntEDc3tq7iCL/OAADkVwEA/tBt9FPqrxLHOPFtyUypppr0\/t6vrl3RrLzCLqqE1nUA/0fmhir2F88KcsxmCJwADo/FglwXGFkjrV4sP6Fj\YBEBzjgEZTWJ1xIKKwYBBAGXVQEFAQEHQCyUIe3sQTaYa/IFNKGNmXz/+hrH\ukcot4TOvi2bD9p8AwEIB8J4BBgWCAAqBYJlNYnXCZA3N7au4gi/zgKbDBYh\BO5iBLnj0J/E6sntEDc3tq7iCL/OAACaFAD7BG3E7TkUoWKtJe5OPzTwX+bM\Xy7hbPSQw0zM9Re8KP0BAIeTG8d280dTK63h/seQAKeMj0zf7AYXr0CscvS7\f38D\=h03E\-----END PGP PUBLIC KEY BLOCK-----"
client_enc_priv_key = r"-----BEGIN PGP PRIVATE KEY BLOCK-----\\xYYEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0\mUR0SKqLmdj+CQMICrS3TNCA/LHgxckC+iTUMxkqQJ9GpXWCDacx1rBQCztu\PDgUHNvWdcvW1wWVxU/aJaQLqBTtRVYkJTz332jrKvsSl/LnrfwmUfKgN4nG\Oc1MUm9ib1NhdHMgSUQgNTUyZGQxYTY2MWE3YWNhNGE0MWY4ODkxMGZmMzRh\YzNiMWFjODBiYjc3OTRlZDlmZDU1ZDhiNzZiOTdhYWQ5N8KMBBAWCgA+BYJl\NYnXBAsJBwgJkDc3tq7iCL/OAxUICgQWAAIBAhkBApsDAh4BFiEE7mIEuePQ\n8Tqye0QNze2ruIIv84AAORXAQD+0G30U+qvEsc48W3JTKmmmvT+3q+uXdGs\vMIuqoTWdQD/R+aGKvYXzwpyzGYInAAOj8WCXBcYWSOtXiw/oWNgEQHHiwRl\NYnXEgorBgEEAZdVAQUBAQdALJQh7exBNphr8gU0oY2ZfP/6Gse6Ryi3hM6+\LZsP2nwDAQgH/gkDCPPoYWyzm4mT4N/TDBF11GVq0xSEEcubFqjArFKyibRy\TDnB8+o8BlkRuGClcfRyKkR5/Rp1v5B0n1BuMsc8nY4Yg4BJv4KhsPfXRp4m\31zCeAQYFggAKgWCZTWJ1wmQNze2ruIIv84CmwwWIQTuYgS549CfxOrJ7RA3\N7au4gi/zgAAmhQA+wRtxO05FKFirSXuTj808F/mzF8u4Wz0kMNMzPUXvCj9\AQCHkxvHdvNHUyut4f7HkACnjI9M3+wGF69ArHL0u39/Aw==\=1hCT\-----END PGP PRIVATE KEY BLOCK-----"
client_pub_key = r"-----BEGIN PGP PUBLIC KEY BLOCK-----\\mDMEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+LFNH+\sw2raQC0TFJvYm9TYXRzIElEIGVkN2QzYjJiMmU1ODlhYjI2NzIwNjA1ZTc0MTRh\YjRmYmNhMjFjYjRiMzFlNWI0ZTYyYTZmYTUxYzI0YTllYWKIjAQQFgoAPgWCZVO9\bwQLCQcICZAuNFtLSY2XJAMVCAoEFgACAQIZAQKbAwIeARYhBDIhViOFpzWovPuw\vC40W0tJjZckAACTeAEA+AdXmA8p6I+FFqXaFVRh5JRa5ZoO4xhGb+QY00kgZisB\AJee8XdW6FHBj2J3b4M9AYqufdpvuj+lLmaVAshN9U4MuDgEZVO9bxIKKwYBBAGX\VQEFAQEHQORkbvSesg9oJeCRKigTNdQ5tkgmVGXfdz/+vwBIl3E3AwEIB4h4BBgW\CAAqBYJlU71vCZAuNFtLSY2XJAKbDBYhBDIhViOFpzWovPuwvC40W0tJjZckAABZ\1AD/RIJM/WNb28pYqtq4XmeOaqLCrbQs2ua8mXpGBZSl8E0BALWSlbHICYTNy9L6\KV0a5pXbxcXpzejcjpJmVwzuWz8P\=32+r\-----END PGP PUBLIC KEY BLOCK-----"
client_enc_priv_key = r"-----BEGIN PGP PRIVATE KEY BLOCK-----\\xYYEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+L\FNH+sw2raQD+CQMIHkZZZnDa6d/gHioGTKf6JevirkCBWwz8tFLGFs5DFwjD\tI4ew9CJd09AUxfMq2WvTilhMNrdw2nmqtmAoaIyIo43azVT1VQoxSDnWxFv\Tc1MUm9ib1NhdHMgSUQgZWQ3ZDNiMmIyZTU4OWFiMjY3MjA2MDVlNzQxNGFi\NGZiY2EyMWNiNGIzMWU1YjRlNjJhNmZhNTFjMjRhOWVhYsKMBBAWCgA+BYJl\U71vBAsJBwgJkC40W0tJjZckAxUICgQWAAIBAhkBApsDAh4BFiEEMiFWI4Wn\Nai8+7C8LjRbS0mNlyQAAJN4AQD4B1eYDynoj4UWpdoVVGHklFrlmg7jGEZv\5BjTSSBmKwEAl57xd1boUcGPYndvgz0Biq592m+6P6UuZpUCyE31TgzHiwRl\U71vEgorBgEEAZdVAQUBAQdA5GRu9J6yD2gl4JEqKBM11Dm2SCZUZd93P/6/\AEiXcTcDAQgH/gkDCGSRul0JyboW4JZSQVlHNVlx2mrfE1gRTh2R5hJWU9Kg\aw2gET8OwWDYU4F8wKTo/s7BGn+HN4jrZeLw1k/etKUKLzuPC06KUXhj3rMF\Ti3CeAQYFggAKgWCZVO9bwmQLjRbS0mNlyQCmwwWIQQyIVYjhac1qLz7sLwu\NFtLSY2XJAAAWdQA/0SCTP1jW9vKWKrauF5njmqiwq20LNrmvJl6RgWUpfBN\AQC1kpWxyAmEzcvS+ildGuaV28XF6c3o3I6SZlcM7ls/Dw==\=YAfZ\-----END PGP PRIVATE KEY BLOCK-----"
# Example valid formatted GPG keys
with open("tests/robots/1/pub_key", "r") as file:

View File

@ -126,6 +126,9 @@ class BalanceLog(models.Model):
def __str__(self):
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
class Meta:
get_latest_by = "time"
class Dispute(models.Model):
pass

View File

@ -21,7 +21,10 @@ services:
network_mode: service:tor
backend:
build: .
build:
context: .
args:
DEVELOPMENT: True
image: backend-image
container_name: django-dev
restart: always
@ -30,7 +33,7 @@ services:
- lnd
- redis
environment:
DEVELOPMENT: 1
DEVELOPMENT: True
volumes:
- .:/usr/src/robosats
- ./node/lnd:/lnd

View File

@ -86,7 +86,7 @@ services:
- cln:/root/.lightning
- ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold
- bitcoin:/root/.bitcoin
command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --bind-addr=127.0.0.1:9737 --max-concurrent-htlcs=483 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true
command: --regtest --wumbo --bitcoin-rpcuser=test --bitcoin-rpcpassword=test --rest-host=0.0.0.0 --rest-port=3010 --bind-addr=127.0.0.1:9737 --max-concurrent-htlcs=483 --grpc-port=9999 --grpc-hold-port=9998 --important-plugin=/root/.lightning/plugins/cln-grpc-hold --database-upgrade=true
depends_on:
- bitcoind
network_mode: service:bitcoind
@ -132,7 +132,10 @@ services:
network_mode: service:bitcoind
coordinator:
build: .
build:
context: .
args:
DEVELOPMENT: True
image: robosats-image
container_name: coordinator
restart: always
@ -142,6 +145,9 @@ services:
USE_TOR: False
MACAROON_PATH: 'data/chain/bitcoin/regtest/admin.macaroon'
CLN_DIR: '/cln/regtest/'
BITCOIND_RPCURL: 'http://127.0.0.1:18443'
BITCOIND_RPCUSER: 'test'
BITCOIND_RPCPASSWORD: 'test'
env_file:
- ${ROBOSATS_ENVS_FILE}
depends_on:

View File

@ -3,10 +3,12 @@ import sys
import time
import requests
from decouple import config
from requests.auth import HTTPBasicAuth
from requests.exceptions import ReadTimeout
wait_step = 0.2
LNVENDOR = config("LNVENDOR", cast=str, default="LND")
WAIT_STEP = 0.2
def get_node(name="robot"):
@ -59,8 +61,8 @@ def wait_for_lnd_node_sync(node_name):
f"\rWaiting for {node_name} node chain sync {round(waited,1)}s"
)
sys.stdout.flush()
waited += wait_step
time.sleep(wait_step)
waited += WAIT_STEP
time.sleep(WAIT_STEP)
def LND_has_active_channels(node_name):
@ -97,8 +99,8 @@ def wait_for_active_channels(lnvendor, node_name="coordinator"):
)
sys.stdout.flush()
waited += wait_step
time.sleep(wait_step)
waited += WAIT_STEP
time.sleep(WAIT_STEP)
def wait_for_cln_node_sync():
@ -112,8 +114,8 @@ def wait_for_cln_node_sync():
f"\rWaiting for coordinator CLN node sync {round(waited,1)}s"
)
sys.stdout.flush()
waited += wait_step
time.sleep(wait_step)
waited += WAIT_STEP
time.sleep(WAIT_STEP)
else:
return
@ -131,8 +133,66 @@ def wait_for_cln_active_channels():
f"\rWaiting for coordinator CLN node channels to be active {round(waited,1)}s"
)
sys.stdout.flush()
waited += wait_step
time.sleep(wait_step)
waited += WAIT_STEP
time.sleep(WAIT_STEP)
def wait_nodes_sync():
wait_for_lnd_node_sync("robot")
if LNVENDOR == "LND":
wait_for_lnd_node_sync("coordinator")
elif LNVENDOR == "CLN":
wait_for_cln_node_sync()
def wait_channels():
wait_for_active_channels(LNVENDOR, "coordinator")
wait_for_active_channels("LND", "robot")
def set_up_regtest_network():
if channel_is_active():
print("Regtest network was already ready. Skipping initalization.")
return
# Fund two LN nodes in regtest and open channels
# Coordinator is either LND or CLN. Robot user is always LND.
if LNVENDOR == "LND":
coordinator_node_id = get_lnd_node_id("coordinator")
coordinator_port = 9735
elif LNVENDOR == "CLN":
coordinator_node_id = get_cln_node_id()
coordinator_port = 9737
print("Coordinator Node ID: ", coordinator_node_id)
# Fund both robot and coordinator nodes
robot_funding_address = create_address("robot")
coordinator_funding_address = create_address("coordinator")
generate_blocks(coordinator_funding_address, 1)
generate_blocks(robot_funding_address, 101)
wait_nodes_sync()
# Open channel between Robot user and coordinator
print(f"\nOpening channel from Robot user node to coordinator {LNVENDOR} node")
connect_to_node("robot", coordinator_node_id, f"localhost:{coordinator_port}")
open_channel("robot", coordinator_node_id, 100_000_000, 50_000_000)
# Generate 10 blocks so the channel becomes active and wait for sync
generate_blocks(robot_funding_address, 10)
# Wait a tiny bit so payments can be done in the new channel
wait_nodes_sync()
wait_channels()
time.sleep(1)
def channel_is_active():
robot_channel_active = LND_has_active_channels("robot")
if LNVENDOR == "LND":
coordinator_channel_active = LND_has_active_channels("coordinator")
elif LNVENDOR == "CLN":
coordinator_channel_active = CLN_has_active_channels()
return robot_channel_active and coordinator_channel_active
def connect_to_node(node_name, node_id, ip_port):
@ -151,7 +211,7 @@ def connect_to_node(node_name, node_id, ip_port):
if "already connected to peer" in response.json()["message"]:
return response.json()
print(f"Could not peer coordinator node: {response.json()}")
time.sleep(wait_step)
time.sleep(WAIT_STEP)
def open_channel(node_name, node_id, local_funding_amount, push_sat):
@ -169,7 +229,7 @@ def open_channel(node_name, node_id, local_funding_amount, push_sat):
return response.json()
def create_address(node_name):
def create_address_LND(node_name):
node = get_node(node_name)
response = requests.get(
f'http://localhost:{node["port"]}/v1/newaddress', headers=node["headers"]
@ -177,6 +237,19 @@ def create_address(node_name):
return response.json()["address"]
def create_address_CLN():
from api.lightning.cln import CLNNode
return CLNNode.newaddress()
def create_address(node_name):
if node_name == "coordinator" and LNVENDOR == "CLN":
return create_address_CLN()
else:
return create_address_LND(node_name)
def generate_blocks(address, num_blocks):
print(f"Mining {num_blocks} blocks")
data = {
@ -199,7 +272,7 @@ def pay_invoice(node_name, invoice):
f'http://localhost:{node["port"]}/v1/channels/transactions',
json=data,
headers=node["headers"],
timeout=1,
timeout=0.3, # 0.15s is enough for LND to LND hodl ACCEPT.
)
except ReadTimeout:
# Request to pay hodl invoice has timed out: that's good!

22
tests/pgp_utils.py Normal file
View File

@ -0,0 +1,22 @@
import gnupg
def sign_message(message, private_key_path, passphrase_path):
gpg = gnupg.GPG()
with open(private_key_path, "r") as f:
private_key = f.read()
with open(passphrase_path, "r") as f:
passphrase = f.read()
gpg.import_keys(private_key, passphrase=passphrase)
# keyid=import_result.fingerprints[0]
signed_message = gpg.sign(
message, passphrase=passphrase, extra_args=["--digest-algo", "SHA512"]
)
# [print(name, getattr(signed_message, name)) for name in dir(signed_message) if not callable(getattr(signed_message, name))]
return signed_message.data.decode(encoding="UTF-8", errors="strict")

View File

@ -1 +1 @@
qz*fp3CzNfK0Y2MWx;<Ke}2&S}ymduQyhjoJtIZE
Y#>]mLP@:Ka2/t_;no*:0GeGd}j2rSQ{}1qwZCED

View File

@ -1,18 +1,18 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
xYYEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0
mUR0SKqLmdj+CQMICrS3TNCA/LHgxckC+iTUMxkqQJ9GpXWCDacx1rBQCztu
PDgUHNvWdcvW1wWVxU/aJaQLqBTtRVYkJTz332jrKvsSl/LnrfwmUfKgN4nG
Oc1MUm9ib1NhdHMgSUQgNTUyZGQxYTY2MWE3YWNhNGE0MWY4ODkxMGZmMzRh
YzNiMWFjODBiYjc3OTRlZDlmZDU1ZDhiNzZiOTdhYWQ5N8KMBBAWCgA+BYJl
NYnXBAsJBwgJkDc3tq7iCL/OAxUICgQWAAIBAhkBApsDAh4BFiEE7mIEuePQ
n8Tqye0QNze2ruIIv84AAORXAQD+0G30U+qvEsc48W3JTKmmmvT+3q+uXdGs
vMIuqoTWdQD/R+aGKvYXzwpyzGYInAAOj8WCXBcYWSOtXiw/oWNgEQHHiwRl
NYnXEgorBgEEAZdVAQUBAQdALJQh7exBNphr8gU0oY2ZfP/6Gse6Ryi3hM6+
LZsP2nwDAQgH/gkDCPPoYWyzm4mT4N/TDBF11GVq0xSEEcubFqjArFKyibRy
TDnB8+o8BlkRuGClcfRyKkR5/Rp1v5B0n1BuMsc8nY4Yg4BJv4KhsPfXRp4m
31zCeAQYFggAKgWCZTWJ1wmQNze2ruIIv84CmwwWIQTuYgS549CfxOrJ7RA3
N7au4gi/zgAAmhQA+wRtxO05FKFirSXuTj808F/mzF8u4Wz0kMNMzPUXvCj9
AQCHkxvHdvNHUyut4f7HkACnjI9M3+wGF69ArHL0u39/Aw==
=1hCT
xYYEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+L
FNH+sw2raQD+CQMIHkZZZnDa6d/gHioGTKf6JevirkCBWwz8tFLGFs5DFwjD
tI4ew9CJd09AUxfMq2WvTilhMNrdw2nmqtmAoaIyIo43azVT1VQoxSDnWxFv
Tc1MUm9ib1NhdHMgSUQgZWQ3ZDNiMmIyZTU4OWFiMjY3MjA2MDVlNzQxNGFi
NGZiY2EyMWNiNGIzMWU1YjRlNjJhNmZhNTFjMjRhOWVhYsKMBBAWCgA+BYJl
U71vBAsJBwgJkC40W0tJjZckAxUICgQWAAIBAhkBApsDAh4BFiEEMiFWI4Wn
Nai8+7C8LjRbS0mNlyQAAJN4AQD4B1eYDynoj4UWpdoVVGHklFrlmg7jGEZv
5BjTSSBmKwEAl57xd1boUcGPYndvgz0Biq592m+6P6UuZpUCyE31TgzHiwRl
U71vEgorBgEEAZdVAQUBAQdA5GRu9J6yD2gl4JEqKBM11Dm2SCZUZd93P/6/
AEiXcTcDAQgH/gkDCGSRul0JyboW4JZSQVlHNVlx2mrfE1gRTh2R5hJWU9Kg
aw2gET8OwWDYU4F8wKTo/s7BGn+HN4jrZeLw1k/etKUKLzuPC06KUXhj3rMF
Ti3CeAQYFggAKgWCZVO9bwmQLjRbS0mNlyQCmwwWIQQyIVYjhac1qLz7sLwu
NFtLSY2XJAAAWdQA/0SCTP1jW9vKWKrauF5njmqiwq20LNrmvJl6RgWUpfBN
AQC1kpWxyAmEzcvS+ildGuaV28XF6c3o3I6SZlcM7ls/Dw==
=YAfZ
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -1 +1 @@
MyopicRacket333
UptightPub730

View File

@ -1,14 +1,14 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0mUR0
SKqLmdi0TFJvYm9TYXRzIElEIDU1MmRkMWE2NjFhN2FjYTRhNDFmODg5MTBmZjM0
YWMzYjFhYzgwYmI3Nzk0ZWQ5ZmQ1NWQ4Yjc2Yjk3YWFkOTeIjAQQFgoAPgWCZTWJ
1wQLCQcICZA3N7au4gi/zgMVCAoEFgACAQIZAQKbAwIeARYhBO5iBLnj0J/E6snt
EDc3tq7iCL/OAADkVwEA/tBt9FPqrxLHOPFtyUypppr0/t6vrl3RrLzCLqqE1nUA
/0fmhir2F88KcsxmCJwADo/FglwXGFkjrV4sP6FjYBEBuDgEZTWJ1xIKKwYBBAGX
VQEFAQEHQCyUIe3sQTaYa/IFNKGNmXz/+hrHukcot4TOvi2bD9p8AwEIB4h4BBgW
CAAqBYJlNYnXCZA3N7au4gi/zgKbDBYhBO5iBLnj0J/E6sntEDc3tq7iCL/OAACa
FAD7BG3E7TkUoWKtJe5OPzTwX+bMXy7hbPSQw0zM9Re8KP0BAIeTG8d280dTK63h
/seQAKeMj0zf7AYXr0CscvS7f38D
=+xY8
mDMEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+LFNH+
sw2raQC0TFJvYm9TYXRzIElEIGVkN2QzYjJiMmU1ODlhYjI2NzIwNjA1ZTc0MTRh
YjRmYmNhMjFjYjRiMzFlNWI0ZTYyYTZmYTUxYzI0YTllYWKIjAQQFgoAPgWCZVO9
bwQLCQcICZAuNFtLSY2XJAMVCAoEFgACAQIZAQKbAwIeARYhBDIhViOFpzWovPuw
vC40W0tJjZckAACTeAEA+AdXmA8p6I+FFqXaFVRh5JRa5ZoO4xhGb+QY00kgZisB
AJee8XdW6FHBj2J3b4M9AYqufdpvuj+lLmaVAshN9U4MuDgEZVO9bxIKKwYBBAGX
VQEFAQEHQORkbvSesg9oJeCRKigTNdQ5tkgmVGXfdz/+vwBIl3E3AwEIB4h4BBgW
CAAqBYJlU71vCZAuNFtLSY2XJAKbDBYhBDIhViOFpzWovPuwvC40W0tJjZckAABZ
1AD/RIJM/WNb28pYqtq4XmeOaqLCrbQs2ua8mXpGBZSl8E0BALWSlbHICYTNy9L6
KV0a5pXbxcXpzejcjpJmVwzuWz8P
=32+r
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,11 +1,11 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
test
bcrt1qrrvml8tr4lkwlqpg9g394tye6s5950qf9tj9e9
-----BEGIN PGP SIGNATURE-----
wnUEARYKACcFgmU22/EJkDc3tq7iCL/OFiEE7mIEuePQn8Tqye0QNze2ruII
v84AAJDMAP9JXQJNRYUiPaSroIfmfJccPQeaVuHTnl0fJqLToL6GbAD/Rt7c
Y67Co6RJi70vytMorPKWmiX6C/mrnKL0auQC8gQ=
=1ouc
iHUEARYIAB0WIQQyIVYjhac1qLz7sLwuNFtLSY2XJAUCZVUUTQAKCRAuNFtLSY2X
JA4zAP9PW71ZvQglGnexa9LYryVbnI0w3WnWXYaOmowy/aMM5wD/a2xZNk95DiDq
s8PnKT41yS+QIBrn7+iZ2DqlCjKdNgc=
=NOcM
-----END PGP SIGNATURE-----

View File

@ -1 +1 @@
gUNa4xT98AA2AQWj4hsdCWFixOmvReu5If3R
C2etfi7nPeUD7rCcwAOy4XoLvEAxbTRGSK6H

View File

@ -1,5 +1,4 @@
import json
import time
from datetime import datetime
from decimal import Decimal
@ -10,25 +9,17 @@ from django.urls import reverse
from api.management.commands.follow_invoices import Command as FollowInvoices
from api.models import Currency, Order
from api.tasks import cache_market
from control.models import BalanceLog
from control.tasks import compute_node_balance
from tests.node_utils import (
CLN_has_active_channels,
LND_has_active_channels,
connect_to_node,
add_invoice,
create_address,
generate_blocks,
get_cln_node_id,
get_lnd_node_id,
open_channel,
pay_invoice,
wait_for_active_channels,
wait_for_cln_node_sync,
wait_for_lnd_node_sync,
set_up_regtest_network,
)
from tests.pgp_utils import sign_message
from tests.test_api import BaseAPITestCase
LNVENDOR = config("LNVENDOR", cast=str, default="LND")
def read_file(file_path):
"""
@ -58,25 +49,6 @@ class TradeTest(BaseAPITestCase):
"longitude": 135.503,
}
def wait_nodes_sync():
wait_for_lnd_node_sync("robot")
if LNVENDOR == "LND":
wait_for_lnd_node_sync("coordinator")
elif LNVENDOR == "CLN":
wait_for_cln_node_sync()
def wait_channels():
wait_for_active_channels("LND", "robot")
wait_for_active_channels(LNVENDOR, "coordinator")
def channel_is_active():
robot_channel_active = LND_has_active_channels("robot")
if LNVENDOR == "LND":
coordinator_channel_active = LND_has_active_channels("coordinator")
elif LNVENDOR == "CLN":
coordinator_channel_active = CLN_has_active_channels()
return robot_channel_active and coordinator_channel_active
@classmethod
def setUpTestData(cls):
"""
@ -88,40 +60,8 @@ class TradeTest(BaseAPITestCase):
# Fetch currency prices from external APIs
cache_market()
# Skip node setup and channel creation if both nodes have an active channel already
if cls.channel_is_active():
print("Regtest network was already ready. Skipping initalization.")
# Take the first node balances snapshot
compute_node_balance()
return
# Fund two LN nodes in regtest and open channels
# Coordinator is either LND or CLN. Robot user is always LND.
if LNVENDOR == "LND":
coordinator_node_id = get_lnd_node_id("coordinator")
coordinator_port = 9735
elif LNVENDOR == "CLN":
coordinator_node_id = get_cln_node_id()
coordinator_port = 9737
print("Coordinator Node ID: ", coordinator_node_id)
funding_address = create_address("robot")
generate_blocks(funding_address, 101)
cls.wait_nodes_sync()
# Open channel between Robot user and coordinator
print(f"\nOpening channel from Robot user node to coordinator {LNVENDOR} node")
connect_to_node("robot", coordinator_node_id, f"localhost:{coordinator_port}")
open_channel("robot", coordinator_node_id, 100_000_000, 50_000_000)
# Generate 10 blocks so the channel becomes active and wait for sync
generate_blocks(funding_address, 10)
# Wait a tiny bit so payments can be done in the new channel
cls.wait_nodes_sync()
cls.wait_channels()
time.sleep(1)
# Initialize bitcoin core, mine some blocks, connect nodes, open channel
set_up_regtest_network()
# Take the first node balances snapshot
compute_node_balance()
@ -155,6 +95,25 @@ class TradeTest(BaseAPITestCase):
usd.timestamp, datetime, "External price timestamp is not a datetime"
)
def test_initial_balance_log(self):
"""
Test if the initial node BalanceLog is correct.
One channel should exist with 0.5BTC in local.
No onchain balance should exist.
"""
balance_log = BalanceLog.objects.latest()
self.assertIsInstance(balance_log.time, datetime)
self.assertTrue(balance_log.total > 0)
self.assertTrue(balance_log.ln_local > 0)
self.assertEqual(balance_log.ln_local_unsettled, 0)
self.assertTrue(balance_log.ln_remote > 0)
self.assertEqual(balance_log.ln_remote_unsettled, 0)
self.assertTrue(balance_log.onchain_total > 0)
self.assertTrue(balance_log.onchain_confirmed > 0)
self.assertEqual(balance_log.onchain_unconfirmed, 0)
self.assertTrue(balance_log.onchain_fraction > 0)
def get_robot_auth(self, robot_index, first_encounter=False):
"""
Create an AUTH header that embeds token, pub_key, and enc_priv_key into a single string
@ -365,12 +324,8 @@ class TradeTest(BaseAPITestCase):
self.assertResponse(response)
self.assertEqual(data["id"], order_made_data["id"])
self.assertTrue(
isinstance(datetime.fromisoformat(data["created_at"]), datetime)
)
self.assertTrue(
isinstance(datetime.fromisoformat(data["expires_at"]), datetime)
)
self.assertIsInstance(datetime.fromisoformat(data["created_at"]), datetime)
self.assertIsInstance(datetime.fromisoformat(data["expires_at"]), datetime)
self.assertTrue(data["is_maker"])
self.assertTrue(data["is_participant"])
self.assertTrue(data["is_buyer"])
@ -382,11 +337,11 @@ class TradeTest(BaseAPITestCase):
self.assertEqual(
data["ur_nick"], read_file(f"tests/robots/{robot_index}/nickname")
)
self.assertTrue(isinstance(data["satoshis_now"], int))
self.assertIsInstance(data["satoshis_now"], int)
self.assertFalse(data["maker_locked"])
self.assertFalse(data["taker_locked"])
self.assertFalse(data["escrow_locked"])
self.assertTrue(isinstance(data["bond_satoshis"], int))
self.assertIsInstance(data["bond_satoshis"], int)
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"])
@ -441,8 +396,8 @@ class TradeTest(BaseAPITestCase):
public_data = json.loads(public_response.content.decode())
self.assertFalse(public_data["is_participant"])
self.assertTrue(isinstance(public_data["price_now"], float))
self.assertTrue(isinstance(data["satoshis_now"], int))
self.assertIsInstance(public_data["price_now"], float)
self.assertIsInstance(data["satoshis_now"], int)
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"])
@ -533,6 +488,7 @@ class TradeTest(BaseAPITestCase):
taker_index = 2
maker_form = self.maker_form_buy_with_range
# Taker GET
response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_index)
data = json.loads(response.content.decode())
@ -547,6 +503,26 @@ class TradeTest(BaseAPITestCase):
self.assertTrue(data["taker_locked"])
self.assertFalse(data["escrow_locked"])
# Maker GET
response = self.get_order(data["id"], maker_index)
data = json.loads(response.content.decode())
self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.WF2).label)
self.assertTrue(data["swap_allowed"])
self.assertIsInstance(data["suggested_mining_fee_rate"], int)
self.assertIsInstance(data["swap_fee_rate"], float)
self.assertTrue(data["suggested_mining_fee_rate"] > 0)
self.assertTrue(data["swap_fee_rate"] > 0)
self.assertEqual(data["maker_status"], "Active")
self.assertEqual(data["taker_status"], "Active")
self.assertTrue(data["is_participant"])
self.assertTrue(data["maker_locked"])
self.assertTrue(data["taker_locked"])
self.assertFalse(data["escrow_locked"])
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"])
@ -561,8 +537,6 @@ class TradeTest(BaseAPITestCase):
# Maker's first order fetch. Should trigger maker bond hold invoice generation.
response = self.get_order(locked_taker_response_data["id"], taker_index)
print("HEREEEEEEEEEEEEEEEEEEEEEEREEEEEEEEEEEEEEEE")
print(response.json())
invoice = response.json()["escrow_invoice"]
# Lock the invoice from the robot's node
@ -597,3 +571,111 @@ class TradeTest(BaseAPITestCase):
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"], 2)
def submit_payout_address(self, order_id, robot_index=1):
path = reverse("order")
params = f"?order_id={order_id}"
headers = self.get_robot_auth(robot_index)
payout_address = create_address("robot")
signed_payout_address = sign_message(
payout_address,
passphrase_path=f"tests/robots/{robot_index}/token",
private_key_path=f"tests/robots/{robot_index}/enc_priv_key",
)
body = {"action": "update_address", "address": signed_payout_address}
response = self.client.post(path + params, body, **headers)
return response
def trade_to_submitted_address(
self, maker_form, take_amount=80, maker_index=1, taker_index=2
):
response_escrow_locked = self.trade_to_locked_escrow(
maker_form, take_amount, maker_index, taker_index
)
response = self.submit_payout_address(
response_escrow_locked.json()["id"], maker_index
)
return response
def test_trade_to_submitted_address(self):
"""
Tests a trade from order creation until escrow locked, before
invoice/address is submitted by buyer.
"""
maker_index = 1
taker_index = 2
maker_form = self.maker_form_buy_with_range
response = self.trade_to_submitted_address(
maker_form, 80, maker_index, taker_index
)
data = response.json()
self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
self.assertFalse(data["is_fiat_sent"])
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"])
def submit_payout_invoice(self, order_id, num_satoshis, robot_index=1):
path = reverse("order")
params = f"?order_id={order_id}"
headers = self.get_robot_auth(robot_index)
payout_invoice = add_invoice("robot", num_satoshis)
signed_payout_invoice = sign_message(
payout_invoice,
passphrase_path=f"tests/robots/{robot_index}/token",
private_key_path=f"tests/robots/{robot_index}/enc_priv_key",
)
body = {"action": "update_invoice", "invoice": signed_payout_invoice}
response = self.client.post(path + params, body, **headers)
return response
def trade_to_submitted_invoice(
self, maker_form, take_amount=80, maker_index=1, taker_index=2
):
response_escrow_locked = self.trade_to_locked_escrow(
maker_form, take_amount, maker_index, taker_index
)
response_get = self.get_order(response_escrow_locked.json()["id"], maker_index)
response = self.submit_payout_invoice(
response_escrow_locked.json()["id"],
response_get.json()["trade_satoshis"],
maker_index,
)
return response
def test_trade_to_submitted_invoice(self):
"""
Tests a trade from order creation until escrow locked, before
invoice/address is submitted by buyer.
"""
maker_index = 1
taker_index = 2
maker_form = self.maker_form_buy_with_range
response = self.trade_to_submitted_invoice(
maker_form, 80, maker_index, taker_index
)
data = response.json()
self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertEqual(data["status_message"], Order.Status(Order.Status.CHA).label)
self.assertFalse(data["is_fiat_sent"])
# Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"])