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 - name: Patch Dockerfile and .env-sample
run: | run: |
sed -i "1s/FROM python:.*/FROM python:${{ matrix.python-tag }}/" Dockerfile 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 sed -i "s/^LNVENDOR=.*/LNVENDOR='${{ matrix.ln-vendor }}'/" .env-sample
- uses: satackey/action-docker-layer-caching@v0.0.11 - uses: satackey/action-docker-layer-caching@v0.0.11

View File

@ -1,5 +1,6 @@
FROM python:3.11.6-slim-bookworm FROM python:3.11.6-slim-bookworm
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG DEVELOPMENT=False
RUN mkdir -p /usr/src/robosats RUN mkdir -p /usr/src/robosats
WORKDIR /usr/src/robosats WORKDIR /usr/src/robosats
@ -17,6 +18,11 @@ RUN python -m pip install --upgrade pip
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r 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 current dir's content to container's WORKDIR root i.e. all the contents of the robosats app
COPY . . COPY . .

View File

@ -79,6 +79,14 @@ class CLNNode:
except Exception as e: except Exception as e:
print(f"Cannot get CLN node id: {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 @classmethod
def decode_payreq(cls, invoice): def decode_payreq(cls, invoice):
"""Decodes a lightning payment request (invoice)""" """Decodes a lightning payment request (invoice)"""

View File

@ -105,10 +105,11 @@ class LNDNode:
lightningstub = lightning_pb2_grpc.LightningStub(cls.channel) lightningstub = lightning_pb2_grpc.LightningStub(cls.channel)
request = lightning_pb2.GetInfoRequest() request = lightning_pb2.GetInfoRequest()
response = lightningstub.GetInfo(request) response = lightningstub.GetInfo(request)
log("lightning_pb2_grpc.GetInfo", request, response)
if response.testnet: if response.testnet:
dummy_address = "tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x" dummy_address = "tb1qehyqhruxwl2p5pt52k6nxj4v8wwc3f3pg7377x"
elif response.chains[0].network == "regtest":
dummy_address = "bcrt1q3w8xja7knmycsglnxg2xzjq8uv9u7jdwau25nl"
else: else:
dummy_address = "bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3" dummy_address = "bc1qgxwaqe4m9mypd7ltww53yv3lyxhcfnhzzvy5j3"
# We assume segwit. Use hardcoded address as shortcut so there is no need of user inputs yet. # 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): def test_validate_pgp_keys(self):
# Example test client generated GPG keys # 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_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-----\\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_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 # Example valid formatted GPG keys
with open("tests/robots/1/pub_key", "r") as file: with open("tests/robots/1/pub_key", "r") as file:

View File

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

View File

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

View File

@ -86,7 +86,7 @@ services:
- cln:/root/.lightning - cln:/root/.lightning
- ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold - ./docker/cln/plugins/cln-grpc-hold:/root/.lightning/plugins/cln-grpc-hold
- bitcoin:/root/.bitcoin - 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: depends_on:
- bitcoind - bitcoind
network_mode: service:bitcoind network_mode: service:bitcoind
@ -132,7 +132,10 @@ services:
network_mode: service:bitcoind network_mode: service:bitcoind
coordinator: coordinator:
build: . build:
context: .
args:
DEVELOPMENT: True
image: robosats-image image: robosats-image
container_name: coordinator container_name: coordinator
restart: always restart: always
@ -142,6 +145,9 @@ services:
USE_TOR: False USE_TOR: False
MACAROON_PATH: 'data/chain/bitcoin/regtest/admin.macaroon' MACAROON_PATH: 'data/chain/bitcoin/regtest/admin.macaroon'
CLN_DIR: '/cln/regtest/' CLN_DIR: '/cln/regtest/'
BITCOIND_RPCURL: 'http://127.0.0.1:18443'
BITCOIND_RPCUSER: 'test'
BITCOIND_RPCPASSWORD: 'test'
env_file: env_file:
- ${ROBOSATS_ENVS_FILE} - ${ROBOSATS_ENVS_FILE}
depends_on: depends_on:

View File

@ -3,10 +3,12 @@ import sys
import time import time
import requests import requests
from decouple import config
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from requests.exceptions import ReadTimeout 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"): 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" f"\rWaiting for {node_name} node chain sync {round(waited,1)}s"
) )
sys.stdout.flush() sys.stdout.flush()
waited += wait_step waited += WAIT_STEP
time.sleep(wait_step) time.sleep(WAIT_STEP)
def LND_has_active_channels(node_name): def LND_has_active_channels(node_name):
@ -97,8 +99,8 @@ def wait_for_active_channels(lnvendor, node_name="coordinator"):
) )
sys.stdout.flush() sys.stdout.flush()
waited += wait_step waited += WAIT_STEP
time.sleep(wait_step) time.sleep(WAIT_STEP)
def wait_for_cln_node_sync(): 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" f"\rWaiting for coordinator CLN node sync {round(waited,1)}s"
) )
sys.stdout.flush() sys.stdout.flush()
waited += wait_step waited += WAIT_STEP
time.sleep(wait_step) time.sleep(WAIT_STEP)
else: else:
return 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" f"\rWaiting for coordinator CLN node channels to be active {round(waited,1)}s"
) )
sys.stdout.flush() sys.stdout.flush()
waited += wait_step waited += WAIT_STEP
time.sleep(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): 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"]: if "already connected to peer" in response.json()["message"]:
return response.json() return response.json()
print(f"Could not peer coordinator node: {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): 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() return response.json()
def create_address(node_name): def create_address_LND(node_name):
node = get_node(node_name) node = get_node(node_name)
response = requests.get( response = requests.get(
f'http://localhost:{node["port"]}/v1/newaddress', headers=node["headers"] f'http://localhost:{node["port"]}/v1/newaddress', headers=node["headers"]
@ -177,6 +237,19 @@ def create_address(node_name):
return response.json()["address"] 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): def generate_blocks(address, num_blocks):
print(f"Mining {num_blocks} blocks") print(f"Mining {num_blocks} blocks")
data = { data = {
@ -199,7 +272,7 @@ def pay_invoice(node_name, invoice):
f'http://localhost:{node["port"]}/v1/channels/transactions', f'http://localhost:{node["port"]}/v1/channels/transactions',
json=data, json=data,
headers=node["headers"], headers=node["headers"],
timeout=1, timeout=0.3, # 0.15s is enough for LND to LND hodl ACCEPT.
) )
except ReadTimeout: except ReadTimeout:
# Request to pay hodl invoice has timed out: that's good! # 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----- -----BEGIN PGP PRIVATE KEY BLOCK-----
xYYEZTWJ1xYJKwYBBAHaRw8BAQdAsfdKb90BurKniu+pBPBDHCkzg08S51W0 xYYEZVO9bxYJKwYBBAHaRw8BAQdAVyePBQK63FB2r5ZpIqO998WaqZjmro+L
mUR0SKqLmdj+CQMICrS3TNCA/LHgxckC+iTUMxkqQJ9GpXWCDacx1rBQCztu FNH+sw2raQD+CQMIHkZZZnDa6d/gHioGTKf6JevirkCBWwz8tFLGFs5DFwjD
PDgUHNvWdcvW1wWVxU/aJaQLqBTtRVYkJTz332jrKvsSl/LnrfwmUfKgN4nG tI4ew9CJd09AUxfMq2WvTilhMNrdw2nmqtmAoaIyIo43azVT1VQoxSDnWxFv
Oc1MUm9ib1NhdHMgSUQgNTUyZGQxYTY2MWE3YWNhNGE0MWY4ODkxMGZmMzRh Tc1MUm9ib1NhdHMgSUQgZWQ3ZDNiMmIyZTU4OWFiMjY3MjA2MDVlNzQxNGFi
YzNiMWFjODBiYjc3OTRlZDlmZDU1ZDhiNzZiOTdhYWQ5N8KMBBAWCgA+BYJl NGZiY2EyMWNiNGIzMWU1YjRlNjJhNmZhNTFjMjRhOWVhYsKMBBAWCgA+BYJl
NYnXBAsJBwgJkDc3tq7iCL/OAxUICgQWAAIBAhkBApsDAh4BFiEE7mIEuePQ U71vBAsJBwgJkC40W0tJjZckAxUICgQWAAIBAhkBApsDAh4BFiEEMiFWI4Wn
n8Tqye0QNze2ruIIv84AAORXAQD+0G30U+qvEsc48W3JTKmmmvT+3q+uXdGs Nai8+7C8LjRbS0mNlyQAAJN4AQD4B1eYDynoj4UWpdoVVGHklFrlmg7jGEZv
vMIuqoTWdQD/R+aGKvYXzwpyzGYInAAOj8WCXBcYWSOtXiw/oWNgEQHHiwRl 5BjTSSBmKwEAl57xd1boUcGPYndvgz0Biq592m+6P6UuZpUCyE31TgzHiwRl
NYnXEgorBgEEAZdVAQUBAQdALJQh7exBNphr8gU0oY2ZfP/6Gse6Ryi3hM6+ U71vEgorBgEEAZdVAQUBAQdA5GRu9J6yD2gl4JEqKBM11Dm2SCZUZd93P/6/
LZsP2nwDAQgH/gkDCPPoYWyzm4mT4N/TDBF11GVq0xSEEcubFqjArFKyibRy AEiXcTcDAQgH/gkDCGSRul0JyboW4JZSQVlHNVlx2mrfE1gRTh2R5hJWU9Kg
TDnB8+o8BlkRuGClcfRyKkR5/Rp1v5B0n1BuMsc8nY4Yg4BJv4KhsPfXRp4m aw2gET8OwWDYU4F8wKTo/s7BGn+HN4jrZeLw1k/etKUKLzuPC06KUXhj3rMF
31zCeAQYFggAKgWCZTWJ1wmQNze2ruIIv84CmwwWIQTuYgS549CfxOrJ7RA3 Ti3CeAQYFggAKgWCZVO9bwmQLjRbS0mNlyQCmwwWIQQyIVYjhac1qLz7sLwu
N7au4gi/zgAAmhQA+wRtxO05FKFirSXuTj808F/mzF8u4Wz0kMNMzPUXvCj9 NFtLSY2XJAAAWdQA/0SCTP1jW9vKWKrauF5njmqiwq20LNrmvJl6RgWUpfBN
AQCHkxvHdvNHUyut4f7HkACnjI9M3+wGF69ArHL0u39/Aw== AQC1kpWxyAmEzcvS+ildGuaV28XF6c3o3I6SZlcM7ls/Dw==
=1hCT =YAfZ
-----END PGP PRIVATE KEY BLOCK----- -----END PGP PRIVATE KEY BLOCK-----

View File

@ -1 +1 @@
MyopicRacket333 UptightPub730

View File

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

View File

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

View File

@ -1 +1 @@
gUNa4xT98AA2AQWj4hsdCWFixOmvReu5If3R C2etfi7nPeUD7rCcwAOy4XoLvEAxbTRGSK6H

View File

@ -1,5 +1,4 @@
import json import json
import time
from datetime import datetime from datetime import datetime
from decimal import Decimal 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.management.commands.follow_invoices import Command as FollowInvoices
from api.models import Currency, Order from api.models import Currency, Order
from api.tasks import cache_market from api.tasks import cache_market
from control.models import BalanceLog
from control.tasks import compute_node_balance from control.tasks import compute_node_balance
from tests.node_utils import ( from tests.node_utils import (
CLN_has_active_channels, add_invoice,
LND_has_active_channels,
connect_to_node,
create_address, create_address,
generate_blocks,
get_cln_node_id,
get_lnd_node_id,
open_channel,
pay_invoice, pay_invoice,
wait_for_active_channels, set_up_regtest_network,
wait_for_cln_node_sync,
wait_for_lnd_node_sync,
) )
from tests.pgp_utils import sign_message
from tests.test_api import BaseAPITestCase from tests.test_api import BaseAPITestCase
LNVENDOR = config("LNVENDOR", cast=str, default="LND")
def read_file(file_path): def read_file(file_path):
""" """
@ -58,25 +49,6 @@ class TradeTest(BaseAPITestCase):
"longitude": 135.503, "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 @classmethod
def setUpTestData(cls): def setUpTestData(cls):
""" """
@ -88,40 +60,8 @@ class TradeTest(BaseAPITestCase):
# Fetch currency prices from external APIs # Fetch currency prices from external APIs
cache_market() cache_market()
# Skip node setup and channel creation if both nodes have an active channel already # Initialize bitcoin core, mine some blocks, connect nodes, open channel
if cls.channel_is_active(): set_up_regtest_network()
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)
# Take the first node balances snapshot # Take the first node balances snapshot
compute_node_balance() compute_node_balance()
@ -155,6 +95,25 @@ class TradeTest(BaseAPITestCase):
usd.timestamp, datetime, "External price timestamp is not a datetime" 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): 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 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.assertResponse(response)
self.assertEqual(data["id"], order_made_data["id"]) self.assertEqual(data["id"], order_made_data["id"])
self.assertTrue( self.assertIsInstance(datetime.fromisoformat(data["created_at"]), datetime)
isinstance(datetime.fromisoformat(data["created_at"]), datetime) self.assertIsInstance(datetime.fromisoformat(data["expires_at"]), datetime)
)
self.assertTrue(
isinstance(datetime.fromisoformat(data["expires_at"]), datetime)
)
self.assertTrue(data["is_maker"]) self.assertTrue(data["is_maker"])
self.assertTrue(data["is_participant"]) self.assertTrue(data["is_participant"])
self.assertTrue(data["is_buyer"]) self.assertTrue(data["is_buyer"])
@ -382,11 +337,11 @@ class TradeTest(BaseAPITestCase):
self.assertEqual( self.assertEqual(
data["ur_nick"], read_file(f"tests/robots/{robot_index}/nickname") 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["maker_locked"])
self.assertFalse(data["taker_locked"]) self.assertFalse(data["taker_locked"])
self.assertFalse(data["escrow_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 # Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"]) self.cancel_order(data["id"])
@ -441,8 +396,8 @@ class TradeTest(BaseAPITestCase):
public_data = json.loads(public_response.content.decode()) public_data = json.loads(public_response.content.decode())
self.assertFalse(public_data["is_participant"]) self.assertFalse(public_data["is_participant"])
self.assertTrue(isinstance(public_data["price_now"], float)) self.assertIsInstance(public_data["price_now"], float)
self.assertTrue(isinstance(data["satoshis_now"], int)) self.assertIsInstance(data["satoshis_now"], int)
# Cancel order to avoid leaving pending HTLCs after a successful test # Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"]) self.cancel_order(data["id"])
@ -533,6 +488,7 @@ class TradeTest(BaseAPITestCase):
taker_index = 2 taker_index = 2
maker_form = self.maker_form_buy_with_range maker_form = self.maker_form_buy_with_range
# Taker GET
response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_index) response = self.make_and_lock_contract(maker_form, 80, maker_index, taker_index)
data = json.loads(response.content.decode()) data = json.loads(response.content.decode())
@ -547,6 +503,26 @@ class TradeTest(BaseAPITestCase):
self.assertTrue(data["taker_locked"]) self.assertTrue(data["taker_locked"])
self.assertFalse(data["escrow_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 # Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"]) self.cancel_order(data["id"])
@ -561,8 +537,6 @@ class TradeTest(BaseAPITestCase):
# Maker's first order fetch. Should trigger maker bond hold invoice generation. # Maker's first order fetch. Should trigger maker bond hold invoice generation.
response = self.get_order(locked_taker_response_data["id"], taker_index) response = self.get_order(locked_taker_response_data["id"], taker_index)
print("HEREEEEEEEEEEEEEEEEEEEEEEREEEEEEEEEEEEEEEE")
print(response.json())
invoice = response.json()["escrow_invoice"] invoice = response.json()["escrow_invoice"]
# Lock the invoice from the robot's node # 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 # Cancel order to avoid leaving pending HTLCs after a successful test
self.cancel_order(data["id"], 2) 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"])