You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

423 lines
19 KiB

import os, json
from twisted.internet import reactor, defer, task
from twisted.web.client import readBody, Headers
from twisted.trial import unittest
from autobahn.twisted.websocket import WebSocketClientFactory, \
connectWS
from jmbase import get_nontor_agent, hextobin, BytesProducer, get_log
from jmbitcoin import CTransaction
from jmclient import (load_test_config, jm_single, SegwitWalletFidelityBonds,
JMWalletDaemon, validate_address, start_reactor)
from jmclient.wallet_rpc import api_version_string
from commontest import make_wallets
from test_coinjoin import make_wallets_to_list, sync_wallets
from test_websocket import (ClientTProtocol, test_tx_hex_1,
test_tx_hex_txid, encoded_token)
testdir = os.path.dirname(os.path.realpath(__file__))
testfileloc = "testwrpc.jmdat"
jlog = get_log()
class JMWalletDaemonT(JMWalletDaemon):
def check_cookie(self, request):
if self.auth_disabled:
return True
return super().check_cookie(request)
class WalletRPCTestBase(object):
""" Base class for set up of tests of the
Wallet RPC calls using the wallet_rpc.JMWalletDaemon service.
"""
# the indices in our wallets to populate
wallet_structure = [1, 3, 0, 0, 0]
# the mean amount of each deposit in the above indices, in btc
mean_amt = 2.0
# the port for the jmwallet daemon
dport = 28183
# the port for the ws
wss_port = 28283
def setUp(self):
load_test_config()
self.clean_out_wallet_file()
jm_single().bc_interface.tick_forward_chain_interval = 5
jm_single().bc_interface.simulate_blocks()
# a client connnection object which is often but not always
# instantiated:
self.client_connector = None
# start the daemon; note we are using tcp connections
# to avoid storing certs in the test env.
# TODO change that.
self.daemon = JMWalletDaemonT(self.dport, self.wss_port, tls=False)
self.daemon.auth_disabled = False
# because we sync and start the wallet service manually here
# (and don't use wallet files yet), we won't have set a wallet name,
# so we set it here:
self.daemon.wallet_name = testfileloc
r, s = self.daemon.startService()
self.listener_rpc = r
self.listener_ws = s
wallet_structures = [self.wallet_structure] * 2
# note: to test fidelity bond wallets we should add the argument
# `wallet_cls=SegwitWalletFidelityBonds` here, but it slows the
# test down from 9 seconds to 1 minute 40s, which is too slow
# to be acceptable. TODO: add a test with FB by speeding up
# the sync for test, by some means or other.
self.daemon.wallet_service = make_wallets_to_list(make_wallets(
1, wallet_structures=[wallet_structures[0]],
mean_amt=self.mean_amt, wallet_cls=SegwitWalletFidelityBonds))[0]
jm_single().bc_interface.tickchain()
sync_wallets([self.daemon.wallet_service])
# dummy tx example to force a notification event:
self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1))
def get_route_root(self):
addr = "http://127.0.0.1:" + str(self.dport)
addr += api_version_string
return addr
def clean_out_wallet_file(self):
if os.path.exists(os.path.join(".", "wallets", testfileloc)):
os.remove(os.path.join(".", "wallets", testfileloc))
def tearDown(self):
self.clean_out_wallet_file()
for dc in reactor.getDelayedCalls():
dc.cancel()
d1 = defer.maybeDeferred(self.listener_ws.stopListening)
d2 = defer.maybeDeferred(self.listener_rpc.stopListening)
if self.client_connector:
self.client_connector.disconnect()
# only fire if everything is finished:
return defer.gatherResults([d1, d2])
class TrialTestWRPC_WS(WalletRPCTestBase, unittest.TestCase):
""" class for testing websocket subscriptions/events etc.
"""
def test_notif(self):
# simulate the daemon already having created
# a valid token (which it usually does when
# starting the WalletService:
self.daemon.wss_factory.valid_token = encoded_token
self.client_factory = WebSocketClientFactory(
"ws://127.0.0.1:"+str(self.wss_port))
self.client_factory.protocol = ClientTProtocol
self.client_connector = connectWS(self.client_factory)
d = task.deferLater(reactor, 0.1, self.fire_tx_notif)
# create a small delay between the instruction to send
# the notification, and the checking of its receipt,
# otherwise the client will be queried before the notification
# arrived:
d.addCallback(self.wait_to_receive)
return d
def wait_to_receive(self, res):
d = task.deferLater(reactor, 0.1, self.checkNotifs)
return d
def checkNotifs(self):
assert self.client_factory.notifs == 1
def fire_tx_notif(self):
self.daemon.wss_factory.sendTxNotification(self.test_tx,
test_tx_hex_txid)
class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase):
@defer.inlineCallbacks
def do_request(self, agent, method, addr, body, handler, token=None):
if token:
headers = Headers({"Authorization": ["Bearer " + self.jwt_token]})
else:
headers = None
response = yield agent.request(method, addr, headers, bodyProducer=body)
yield self.response_handler(response, handler)
@defer.inlineCallbacks
def response_handler(self, response, handler):
body = yield readBody(response)
# these responses should always be 200 OK.
assert response.code == 200
# handlers check the body is as expected; no return.
yield handler(body)
return True
@defer.inlineCallbacks
def test_create_list_lock_unlock(self):
""" A batch of tests in sequence here,
so we can track the state of a created
wallet and check it is what is expected.
We test create first, so we have a wallet.
1. create a wallet and have it persisted
to disk in ./wallets, and get a token.
2. list wallets and check they contain the new
wallet.
3. lock the existing wallet service, using the token.
4. Unlock the wallet with /unlock, get a token.
"""
# before starting, we have to shut down the existing
# wallet service (usually this would be `lock`):
self.daemon.wallet_service = None
self.daemon.stopService()
self.daemon.auth_disabled = False
agent = get_nontor_agent()
root = self.get_route_root()
addr = root + "/wallet/create"
addr = addr.encode()
body = BytesProducer(json.dumps({"walletname": testfileloc,
"password": "hunter2", "wallettype": "sw-fb"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_create_wallet_response)
addr = root + "/wallet/all"
addr = addr.encode()
# does not require a token, though we just got one.
yield self.do_request(agent, b"GET", addr, None,
self.process_list_wallets_response)
# now *lock* the existing, which will shut down the wallet
# service associated.
addr = root + "/wallet/" + self.daemon.wallet_name + "/lock"
addr = addr.encode()
jlog.info("Using address: {}".format(addr))
yield self.do_request(agent, b"GET", addr, None,
self.process_lock_response, token=self.jwt_token)
# wallet service should now be stopped.
addr = root + "/wallet/" + self.daemon.wallet_name + "/unlock"
addr = addr.encode()
body = BytesProducer(json.dumps({"password": "hunter2"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_unlock_response)
def process_create_wallet_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
self.jwt_token = json_body["token"]
# we don't use this in test, but it must exist:
assert json_body["seedphrase"]
def process_list_wallets_response(self, body):
json_body = json.loads(body.decode("utf-8"))
assert json_body["wallets"] == [testfileloc]
@defer.inlineCallbacks
def test_direct_send_and_display_wallet(self):
""" First spend a coin, then check the balance
via the display wallet output.
"""
self.daemon.auth_disabled = True
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/taker/direct-send"
addr = addr.encode()
body = BytesProducer(json.dumps({"mixdepth": "1",
"amount_sats": "100000000",
"destination": "2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_direct_send_response)
# force the wallet service txmonitor to wake up, to see the new
# tx before querying /display:
self.daemon.wallet_service.transaction_monitor()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/display"
addr = addr.encode()
yield self.do_request(agent, b"GET", addr, None,
self.process_wallet_display_response)
def process_direct_send_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert "txinfo" in json_body
# TODO tx check
print(json_body["txinfo"])
def process_wallet_display_response(self, response):
json_body = json.loads(response.decode("utf-8"))
latest_balance = float(json_body["walletinfo"]["total_balance"])
jlog.info("Wallet display currently shows balance: {}".format(
latest_balance))
assert latest_balance > self.mean_amt * 4.0 - 1.1
assert latest_balance <= self.mean_amt * 4.0 - 1.0
@defer.inlineCallbacks
def test_getaddress(self):
""" Tests that we can source a valid address
for deposits using getaddress.
"""
self.daemon.auth_disabled = True
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/address/new/3"
addr = addr.encode()
yield self.do_request(agent, b"GET", addr, None,
self.process_new_addr_response)
@defer.inlineCallbacks
def test_gettimelockaddress(self):
self.daemon.auth_disabled = True
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/address/timelock/new/2023-02"
addr = addr.encode()
yield self.do_request(agent, b"GET", addr, None,
self.process_new_addr_response)
def process_new_addr_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert validate_address(json_body["address"])[0]
@defer.inlineCallbacks
def test_listutxos(self):
self.daemon.auth_disabled = True
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/utxos"
addr = addr.encode()
yield self.do_request(agent, b"GET", addr, None,
self.process_listutxos_response)
def process_listutxos_response(self, response):
json_body = json.loads(response.decode("utf-8"))
# some fragility in test structure here: what utxos we
# have depend on what other tests occurred.
# For now, we at least check that we have 3 utxos in mixdepth
# 1 because none of the other tests spend them:
mixdepth1_utxos = 0
for d in json_body["utxos"]:
if d["mixdepth"] == 1:
mixdepth1_utxos += 1
assert mixdepth1_utxos == 3
@defer.inlineCallbacks
def test_session(self):
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/session"
addr = addr.encode()
yield self.do_request(agent, b"GET", addr, None,
self.process_session_response)
def process_session_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert json_body["maker_running"] is False
assert json_body["coinjoin_in_process"] is False
def process_unlock_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
self.jwt_token = json_body["token"]
def process_lock_response(self, response):
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
@defer.inlineCallbacks
def test_do_coinjoin(self):
""" This slightly weird test curently only
tests *requesting* a coinjoin; because there are
no makers running in the test suite, the Taker will
give up early due to the empty orderbook, but that is
OK since this API call only makes the request.
"""
self.daemon.auth_disabled = True
# in normal operations, the RPC call will trigger
# the jmclient to connect to an *existing* daemon
# that was created on startup, but here, that daemon
# does not yet exist, so we will get 503 Backend Not Ready,
# unless we manually create it:
scon, ccon = start_reactor(jm_single().config.get("DAEMON",
"daemon_host"), jm_single().config.getint("DAEMON",
"daemon_port"), None, daemon=True, rs=False)
# must be manually set:
self.scon = scon
agent = get_nontor_agent()
addr = self.get_route_root()
addr += "/wallet/"
addr += self.daemon.wallet_name
addr += "/taker/coinjoin"
addr = addr.encode()
body = BytesProducer(json.dumps({"mixdepth": "1",
"amount_sats": "22000000",
"counterparties": "2",
"destination": "2N2JD6wb56AfK4tfmM6PwdVmoYk2dCKf4Br"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_do_coinjoin_response)
def process_do_coinjoin_response(self, response):
# response code is already checked to be 200
clientconn = self.daemon.coinjoin_connection
# backend's AMP connection must be cleaned up, otherwise
# test will fail for unclean reactor:
self.addCleanup(clientconn.disconnect)
self.addCleanup(self.scon.stopListening)
assert json.loads(response.decode("utf-8")) == {}
"""
Sample listutxos response for reference:
{
"utxos": [{
"utxo": "e01f349b1b5659c01f09ec70ca418a26d34f573e13f878db46dff39763e4dd15:0",
"address": "bcrt1qxgqw54x46kmkkg6g23kdfuy76mfhc4m88shg4n",
"value": 200000000,
"tries": 0,
"tries_remaining": 3,
"external": false,
"mixdepth": 0,
"confirmations": 5,
"frozen": false
}, {
"utxo": "eba94a0011e0f3f97a9c49be7f6ae38eb75bbeacd8c1797425e9005d80ec2f70:0",
"address": "bcrt1qz5p304dj54g9nxh87afyvwpkv0jd3lydka6nfp",
"value": 200000000,
"tries": 0,
"tries_remaining": 3,
"external": false,
"mixdepth": 1,
"confirmations": 4,
"frozen": false
}, {
"utxo": "fd5f181f1c1d1d47f3f110c3426769e60450e779addabf3f57f1732099ecdf97:0",
"address": "bcrt1qu7k4dppungsqp95nwc7ansqs9m0z95h72j9mze",
"value": 200000000,
"tries": 0,
"tries_remaining": 3,
"external": false,
"mixdepth": 1,
"confirmations": 3,
"frozen": false
}, {
"utxo": "03de36659e18068d272e182b2a57fdf8364d0d8c9aaf1b8c971a1590fa983cd5:0",
"address": "bcrt1qk0thvwz8djvnynv2cmq7706ff9tjxcjef3cr7l",
"value": 200000000,
"tries": 0,
"tries_remaining": 3,
"external": false,
"mixdepth": 1,
"confirmations": 2,
"frozen": false
}]
}
"""
"""
Sample displaywallet response for reference:
[{"succeed": true, "status": 200, "walletname": "testwrpc.jmdat", "walletinfo": {"wallet_name": "JM wallet", "total_balance": "6.99998570", "accounts": [{"account": "0", "account_balance": "2.00000000", "branches": [{"branch": "external addresses\tm/84'/1'/0'/0\ttpubDExGchYUujKhNNYvVMjW6S9X4B3Cd3mNqm19vknwovH8buM7GJACi6gCi8Qc9Q9ejBx7phVRUrJFNT5GwpcUSTLqEKNbdCEaKLMdKfgp6Yd", "balance": "2.00000000", "entries": [{"hd_path": "m/84'/1'/0'/0/0", "address": "bcrt1qk4txxx2xzdz8y6yg2w60l9lea6h3k3el7jqnxk", "amount": "2.00000000", "labels": "used"}]}, {"branch": "internal addresses\tm/84'/1'/0'/1\t", "balance": "0.00000000", "entries": []}]}, {"account": "1", "account_balance": "4.99998570", "branches": [{"branch": "external addresses\tm/84'/1'/1'/0\ttpubDET2QAFuGCcmMhzJ6E7yTKUD5Fc8PqnL81yxmb2YZuWcG2MmhoUjLERK7S2gwyGPM1wiaCxWRjWXjnw3KgC9X2wMN38YRj3z4yz43HoMP67", "balance": "4.00000000", "entries": [{"hd_path": "m/84'/1'/1'/0/0", "address": "bcrt1qyqa9sawgwmkpy3pg599mv6peyg9uag8s2pdkpr", "amount": "2.00000000", "labels": "used"}, {"hd_path": "m/84'/1'/1'/0/1", "address": "bcrt1q0ky7pwdzpftd3jy6w6rt8krap2tsrcuzjte69y", "amount": "2.00000000", "labels": "used"}]}, {"branch": "internal addresses\tm/84'/1'/1'/1\t", "balance": "0.99998570", "entries": [{"hd_path": "m/84'/1'/1'/1/0", "address": "bcrt1qjdnnz5w75upqquvcsksyyeq0u9c2m5j9eld0nf", "amount": "0.99998570", "labels": "used"}]}]}, {"account": "2", "account_balance": "0.00000000", "branches": [{"branch": "external addresses\tm/84'/1'/2'/0\ttpubDEGRBmiDr2tqdcQFCVykULPzmuvTUeXCrG6w7C46wp7wrncU1hPpSzoYKn44kw6J6i5doWLSx8bzkjBeh8HvqRVPzJBetuq5xeV2iFWwS6q", "balance": "0.00000000", "entries": []}, {"branch": "internal addresses\tm/84'/1'/2'/1\t", "balance": "0.00000000", "entries": []}]}, {"account": "3", "account_balance": "0.00000000", "branches": [{"branch": "external addresses\tm/84'/1'/3'/0\ttpubDFa44cU854x2qYsHgWU1CFNaNRyQwaceXEHb41BEWw97KMmpaWP9JrbdF3mnzCq1se8GbnT5Ra7erPrh8vSCCNqPUsmsahYVZ3dgVg19dWF", "balance": "0.00000000", "entries": []}, {"branch": "internal addresses\tm/84'/1'/3'/1\t", "balance": "0.00000000", "entries": []}]}, {"account": "4", "account_balance": "0.00000000", "branches": [{"branch": "external addresses\tm/84'/1'/4'/0\ttpubDFK8hTjQBCEz3aaiDeyucPX56DBZprCpJZ5Jrb2cHiWDTudBTYtj6EHSxXypnQQFPAfJH6zVVnC6YzeHBsc79XErY1AkQrJkayySMhKhQbK", "balance": "0.00000000", "entries": []}, {"branch": "internal addresses\tm/84'/1'/4'/1\t", "balance": "0.00000000", "entries": []}]}]}}]
"""