mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 02:46:28 +00:00
Add tests for onchain address, pgp sign verification. Improve Dockerfile
This commit is contained in:
parent
79a3df66a2
commit
3e0d451e97
1
.github/workflows/integration-tests.yml
vendored
1
.github/workflows/integration-tests.yml
vendored
@ -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
|
||||
|
@ -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 . .
|
||||
|
||||
|
@ -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)"""
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
22
tests/pgp_utils.py
Normal 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")
|
@ -1 +1 @@
|
||||
qz*fp3CzNfK0Y2MWx;<Ke}2&S}ymduQyhjoJtIZE
|
||||
Y#>]mLP@:Ka2/t_;no*:0GeGd}j2rSQ{}1qwZCED
|
@ -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-----
|
@ -1 +1 @@
|
||||
MyopicRacket333
|
||||
UptightPub730
|
@ -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-----
|
||||
|
@ -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-----
|
||||
|
@ -1 +1 @@
|
||||
gUNa4xT98AA2AQWj4hsdCWFixOmvReu5If3R
|
||||
C2etfi7nPeUD7rCcwAOy4XoLvEAxbTRGSK6H
|
@ -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"])
|
||||
|
Loading…
Reference in New Issue
Block a user