async def test_subscriptions_without_signing(self): val = 761751855997712 ws_con = await self.websocket_connect(None) await ws_con.call("subscribe", [TEST_ID_ADDRESS]) async with self.pool.acquire() as con: row = await con.fetchrow( "SELECT COUNT(*) FROM notification_registrations WHERE eth_address = $1", TEST_ID_ADDRESS) self.assertEqual(row['count'], 1) tx_hash = await self.faucet(TEST_ID_ADDRESS, val) result = await ws_con.read() self.assertIsNotNone(result) payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) ws_con.con.close() ws_con = await self.websocket_connect(None) await self.faucet(TEST_ID_ADDRESS, val) # make sure we don't still get any notifications result = await ws_con.read(timeout=1) self.assertIsNone(result) # make sure subscriptions don't cross over when no toshi id is used await ws_con.call("subscribe", [TEST_WALLET_ADDRESS]) ws_con2 = await self.websocket_connect(None) tx_hash = await self.faucet(TEST_WALLET_ADDRESS, val) result = await ws_con.read() self.assertIsNotNone(result) payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) # check 2nd connection has no notifications result = await ws_con2.read(timeout=1) self.assertIsNone(result) # make sure unsubscribe works await ws_con.call("unsubscribe", [TEST_WALLET_ADDRESS]) await self.faucet(TEST_WALLET_ADDRESS, val) result = await ws_con.read(timeout=1) # handle case where confirmed comes in before the unsibscribe if result: payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) self.assertEqual(payment['status'], 'confirmed') result = await ws_con.read(timeout=1) self.assertIsNone(result)
async def test_get_balance(self, *, ethminer): addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb' val = 8761751855997712 val2 = int(val / 2) await self.wait_on_tx_confirmation(await self.faucet(TEST_ID_ADDRESS, val)) ws_con = await self.websocket_connect(TEST_ID_KEY) result = await ws_con.call("get_balance", [TEST_ID_ADDRESS]) self.assertEqual(int(result['confirmed_balance'][2:], 16), val) self.assertEqual(int(result['unconfirmed_balance'][2:], 16), val) # make sure no blocks get mined for a bit ethminer.pause() tx = create_transaction(nonce=0x100000, gasprice=DEFAULT_GASPRICE, startgas=DEFAULT_STARTGAS, to=addr, value=val2, network_id=self.network_id) tx = sign_transaction(tx, TEST_ID_KEY) tx = encode_transaction(tx) await ws_con.call("subscribe", [addr]) tx_hash = await ws_con.call("send_transaction", {"tx": tx}) new_balance = val - (val2 + DEFAULT_STARTGAS * DEFAULT_GASPRICE) result = await ws_con.call("get_balance", [TEST_ID_ADDRESS]) self.assertEqual(int(result['confirmed_balance'][2:], 16), val) self.assertEqual(int(result['unconfirmed_balance'][2:], 16), new_balance) # check for the unconfirmed notification result = await ws_con.read() self.assertIsNotNone(result) payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) self.assertEqual(payment['status'], 'unconfirmed') # restart mining ethminer.start() result = await ws_con.read() payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) self.assertEqual(payment['status'], 'confirmed') result = await ws_con.call("get_balance", [TEST_ID_ADDRESS]) self.assertEqual(int(result['confirmed_balance'][2:], 16), new_balance, "int('{}', 16) != {}".format(result['confirmed_balance'], new_balance)) self.assertEqual(int(result['unconfirmed_balance'][2:], 16), new_balance, "int('{}', 16) != {}".format(result['unconfirmed_balance'], new_balance))
async def test_subscriptions(self): val = 761751855997712 ws_con = await self.websocket_connect(TEST_ID_KEY) await ws_con.call("subscribe", [TEST_ID_ADDRESS]) async with self.pool.acquire() as con: row = await con.fetchrow( "SELECT COUNT(*) FROM notification_registrations WHERE toshi_id = $1", TEST_ID_ADDRESS) self.assertEqual(row['count'], 1) tx_hash = await self.faucet(TEST_ID_ADDRESS, val) result = await ws_con.read() self.assertIsNotNone(result) payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) ws_con.con.close() ws_con = await self.websocket_connect(TEST_ID_KEY) await self.faucet(TEST_ID_ADDRESS, val) # make sure we don't still get any notifications result = await ws_con.read(timeout=1) self.assertIsNone(result) # make sure subscriptions to a different address from the toshi id work await ws_con.call("subscribe", [TEST_WALLET_ADDRESS]) tx_hash = await self.faucet(TEST_WALLET_ADDRESS, val) result = await ws_con.read() self.assertIsNotNone(result) payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) await ws_con.call("unsubscribe", [TEST_WALLET_ADDRESS]) await self.faucet(TEST_WALLET_ADDRESS, val) result = await ws_con.read(timeout=1) # handle case where confirmed comes in before the unsibscribe if result: payment = parse_sofa_message(result['params']['message']) self.assertEqual(payment['txHash'], tx_hash) self.assertEqual(payment['status'], 'confirmed') result = await ws_con.read(timeout=1) self.assertIsNone(result)
async def test_redis_failures(self, *, redis_server, push_client): """Tests that the block monitor recovers correctly after errors in the ethereum node""" addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb' val = 761751855997712 body = { "registration_id": TEST_GCM_ID, "address": TEST_ID_ADDRESS } resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) tx_hash = await self.faucet(TEST_ID_ADDRESS, val * 1000) for status in ['unconfirmed', 'confirmed']: _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) if status == 'unconfirmed' and message['status'] == 'confirmed': # this can happen if the monitor sees the confirmed message # first as the faucet sends raw txs break self.assertEqual(message['status'], status) # do this 10 times to see if the pool is working as expected as well for _ in range(0, 10): # kill the server! redis_server.pause() await asyncio.sleep(0.1) # start it again redis_server.start() # see if everything still works tx_hash = await self.send_tx(TEST_ID_KEY, addr, 10 ** 10) # wait for 2 messages, as the confirmed one should come # from the block monitor for status in ['unconfirmed', 'confirmed']: _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['status'], status)
async def test_get_sofa_payment(self): body = { "from": FAUCET_ADDRESS, "to": TEST_ADDRESS, "value": 10 ** 10 } resp = await self.fetch("/tx/skel", method="POST", body=body) self.assertEqual(resp.code, 200) body = json_decode(resp.body) tx = sign_transaction(body['tx'], FAUCET_PRIVATE_KEY) resp = await self.fetch("/tx", method="POST", body={ "tx": tx }) self.assertEqual(resp.code, 200, resp.body) body = json_decode(resp.body) tx_hash = body['tx_hash'] await self.wait_on_tx_confirmation(tx_hash) resp = await self.fetch("/tx/{}?format=sofa".format(tx_hash), method="GET") self.assertEqual(resp.code, 200, resp.body) message = parse_sofa_message(resp.body.decode('utf-8')) self.assertEqual(message["txHash"], tx_hash) self.assertEqual(message["status"], "confirmed")
async def test_sending_transactions_and_storing_the_hash_correctly( self, *, push_client): """This test is born out of the fact that `UnsingedTransaction.hash` always calculates the hash of the transaction without the signature, even after `.sign` has been called on the transaction. This caused errors in what was being stored in the database and incorrectly detecting transaction overwrites. This test exposed the behaviour correctly so that it could be fixed""" # register for GCM PNs body = {"registration_id": TEST_GCM_ID, "address": TEST_ID_ADDRESS} resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) body = {"from": FAUCET_ADDRESS, "to": TEST_ID_ADDRESS, "value": 10**10} resp = await self.fetch("/tx/skel", method="POST", body=body) self.assertEqual(resp.code, 200) body = json_decode(resp.body) tx = decode_transaction(body['tx']) tx = sign_transaction(tx, FAUCET_PRIVATE_KEY) sig = signature_from_transaction(tx) body = {"tx": body['tx'], "signature": data_encoder(sig)} resp = await self.fetch("/tx", method="POST", body=body) self.assertEqual(resp.code, 200, resp.body) body = json_decode(resp.body) tx_hash = body['tx_hash'] async def check_db(): async with self.pool.acquire() as con: rows = await con.fetch( "SELECT * FROM transactions WHERE nonce = $1", tx.nonce) self.assertEqual(len(rows), 1) self.assertEqual(rows[0]['hash'], tx_hash) if rows[0]['status'] is not None: self.assertEqual(rows[0]['status'], 'unconfirmed') self.assertIsNone(rows[0]['error']) await self.wait_on_tx_confirmation(tx_hash, check_db) while True: token, payload = await push_client.get() message = parse_sofa_message(payload['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) if message['status'] == "confirmed": break
async def test_get_multiple_push_notification(self, *, push_client): # register for GCM PNs body = {"registration_id": TEST_GCM_ID, "address": TEST_WALLET_ADDRESS} resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) # register 2nd device body = { "registration_id": TEST_GCM_ID_2, "address": TEST_WALLET_ADDRESS } resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) async with self.pool.acquire() as con: rows = await con.fetch( "SELECT * FROM notification_registrations WHERE toshi_id = $1", TEST_ID_ADDRESS) self.assertIsNotNone(rows) self.assertEqual(len(rows), 2) value = 2821181018869341261 tx_hash = await self.faucet(TEST_WALLET_ADDRESS, value) unconfirmed_counts = {TEST_GCM_ID: 0, TEST_GCM_ID_2: 0} while True: token, payload = await push_client.get() self.assertTrue(token in (TEST_GCM_ID, TEST_GCM_ID_2)) message = parse_sofa_message(payload['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['value'], hex(value)) self.assertEqual(message['txHash'], tx_hash) if message['status'] == "confirmed": break self.assertEqual(message['status'], "unconfirmed") unconfirmed_counts[token] += 1 if unconfirmed_counts[token] > 1: warnings.warn( "got more than one unconfirmed notification for a single device" )
async def test_eth_nodes_out_of_sync_on_confirm(self, *, monitor, push_client): # shutdown the monitor so that the confirmed transaction status can be # manually triggered before the transaction is actually confirmed await monitor.shutdown() val = 761751855997712 body = {"registration_id": TEST_GCM_ID, "address": TEST_ID_ADDRESS} resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) tx_hash = await self.send_tx(FAUCET_PRIVATE_KEY, TEST_ID_ADDRESS, val) tx_id = None while tx_id is None: async with self.pool.acquire() as con: tx_id = await con.fetchval( "SELECT transaction_id FROM transactions WHERE hash = $1", tx_hash) # force confirmed, even though it should take some time manager_dispatcher.update_transaction(tx_id, 'confirmed') _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['status'], 'unconfirmed') _, pn = await push_client.get() message = parse_sofa_message(pn['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['status'], 'confirmed')
async def test_erc20_balance_update(self, *, parity, push_client, monitor): """Tests that on initial PN registration the user's token cache is updated Creates 4 erc20 tokens, gives a test address some of 3 of those tokens, registeres that address for PNs, and checks that the balance cache is updated """ token_args = [ ["TST", "Test Token", 18], ["BOB", "Big Old Bucks", 10], ["HMM", "Hmmmmmm", 5], ["NOT", "Not This One", 20] ] tokens = {} contracts = {} for args in token_args: contract = await self.deploy_erc20_contract(*args) contracts[contract.address] = contract tokens[contract.address] = {"symbol": args[0], "name": args[1], "decimals": args[2], "contract": contract} args.append(contract.address) for token in tokens.values(): if token['symbol'] == token_args[-1][0]: continue # give "1" of each token (except NOT) contract = token['contract'] await contract.transfer.set_sender(FAUCET_PRIVATE_KEY)(TEST_ADDRESS, 10 ** token['decimals']) result = await contract.balanceOf(TEST_ADDRESS) self.assertEquals(result, 10 ** token['decimals']) # force block check to clear out txs pre registration await monitor.block_check() resp = await self.fetch_signed("/apn/register", signing_key=TEST_PRIVATE_KEY, method="POST", body={ "registration_id": TEST_APN_ID }) self.assertEqual(resp.code, 204) # get user's initial token balance await asyncio.sleep(0.1) resp = await self.fetch("/tokens/{}".format(TEST_ADDRESS)) self.assertResponseCodeEqual(resp, 200) body = json_decode(resp.body) self.assertEqual(len(body['tokens']), len(token_args) - 1) for balance in body['tokens']: self.assertEqual(int(balance['value'], 16), 10 ** tokens[balance['contract_address']]['decimals']) await self.send_tx(FAUCET_PRIVATE_KEY, TEST_ADDRESS, 10 ** 18) # wait for unconfirmed and confirmed PN, otherwise the contract send will overwrite it (TODO) await push_client.get() await push_client.get() # test that receiving new tokens triggers a PN for token in tokens.values(): contract = token['contract'] # first test PNs from external transactions tx_hash = await contract.transfer.set_sender(FAUCET_PRIVATE_KEY)(TEST_ADDRESS, 10 ** token['decimals'], wait_for_confirmation=False) pn = await push_client.get() sofa = parse_sofa_message(pn[1]['message']) self.assertEqual(sofa['status'], 'confirmed') self.assertEqual(sofa['txHash'], tx_hash) self.assertEqual(sofa.type, "TokenPayment") self.assertEqual(sofa['contractAddress'], contract.address) self.assertEqual(sofa['value'], hex(10 ** token['decimals'])) self.assertEqual(sofa['toAddress'], TEST_ADDRESS) # now test PNs from toshi generated transactions raw_tx = await contract.transfer.get_raw_tx.set_sender(FAUCET_PRIVATE_KEY)(TEST_ADDRESS, 10 ** token['decimals']) tx_hash = await self.send_raw_tx(raw_tx, wait_on_tx_confirmation=False) pn = await push_client.get() sofa = parse_sofa_message(pn[1]['message']) self.assertEqual(sofa['status'], 'confirmed') self.assertEqual(sofa['txHash'], tx_hash) self.assertEqual(sofa.type, "TokenPayment") self.assertEqual(sofa['contractAddress'], contract.address) self.assertEqual(sofa['value'], hex(10 ** token['decimals'])) self.assertEqual(sofa['toAddress'], TEST_ADDRESS) await asyncio.sleep(0.1) async with self.pool.acquire() as con: balance = await con.fetchrow("SELECT * FROM token_balances WHERE eth_address = $1 AND contract_address = $2", TEST_ADDRESS, contract.address) self.assertEqual(int(balance['value'], 16), (10 ** token['decimals']) * (3 if token['symbol'] != token_args[-1][0] else 2), "invalid balance after updating {} token".format(token['symbol']))
async def test_list_payment_updates(self): addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb' thetime = int(time.time()) # make sure half of the unconfirmed txs stime = thetime - 300 async def store_tx(status, created, updated=None): tx_hash = data_encoder(os.urandom(64)) from_addr = data_encoder(os.urandom(20)) value = random.randint(10**15, 10**20) if updated is None: updated = created async with self.pool.acquire() as con: await con.execute( "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status, created, updated) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", tx_hash, from_addr, addr, 1, hex(value), hex(DEFAULT_STARTGAS), hex(DEFAULT_GASPRICE), status, datetime.utcfromtimestamp(created), datetime.utcfromtimestamp(updated)) txs_per_state = 5 # create 5 transactions that were created before the start time, but confirmed after created = stime - 30 updated = stime + 30 for i in range(0, txs_per_state): await store_tx('confirmed', created + i, updated + i) # create 5 transactions that were both created and confirmed during the requested period created = stime + 30 updated = stime + 60 for i in range(0, txs_per_state): await store_tx('confirmed', created + i, updated + i) # create 5 transactions created but not confirmed during the requested period created = stime + 60 for i in range(0, txs_per_state): await store_tx('unconfirmed', created + i) # create 5 transactions outside of the requested period created = thetime for i in range(0, txs_per_state): await store_tx('unconfirmed', created + i) ws_con = await self.websocket_connect(TEST_ID_KEY) result = await ws_con.call("list_payment_updates", [addr, stime, thetime]) # expect 5 confirmed payments c = 0 for i in range(0, txs_per_state): self.assertEqual( parse_sofa_message(result[c + i])['status'], 'confirmed') # expect 5 txs with both unconfirmed and confirmed c += txs_per_state for i in range(0, txs_per_state): self.assertEqual( parse_sofa_message(result[c + (i * 2)])['status'], 'unconfirmed') self.assertEqual( parse_sofa_message(result[c + (i * 2) + 1])['status'], 'confirmed') c += txs_per_state * 2 # expect 5 unconfirmed txs for i in range(0, txs_per_state): self.assertEqual( parse_sofa_message(result[c + i])['status'], 'unconfirmed') # make sure there's no more! c += txs_per_state self.assertEqual(len(result), c)
async def test_get_single_push_notification(self, *, push_client): """makes sure both source and target of a transaction get a confirmation push notification""" # register for GCM PNs body = { "registration_id": TEST_GCM_ID, "address": TEST_WALLET_ADDRESS } resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) # register source account for pn body = { "registration_id": TEST_GCM_ID_2, "address": FAUCET_ADDRESS } resp = await self.fetch_signed("/gcm/register", signing_key=FAUCET_PRIVATE_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) async with self.pool.acquire() as con: rows1 = await con.fetch("SELECT * FROM notification_registrations WHERE toshi_id = $1", TEST_ID_ADDRESS) rows2 = await con.fetch("SELECT * FROM notification_registrations WHERE toshi_id = $1", FAUCET_ADDRESS) self.assertEqual(len(rows1), 1) self.assertEqual(len(rows2), 1) value = 2821181018869341261 tx_hash = await self.send_tx(FAUCET_PRIVATE_KEY, TEST_WALLET_ADDRESS, value) unconfirmed_count = 0 confirmed_1 = None unconfirmed_1 = None while True: token, payload = await push_client.get() self.assertIn(token, [TEST_GCM_ID, TEST_GCM_ID_2]) message = parse_sofa_message(payload['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['value'], hex(value)) self.assertEqual(message['txHash'], tx_hash) self.assertEqual(message['networkId'], '66') if message['status'] == "confirmed": # ensures that the 2 expected confirmed pns are # meant for the different targets if confirmed_1: self.assertNotEqual(token, confirmed_1) break else: confirmed_1 = token continue self.assertEqual(message['status'], "unconfirmed") if unconfirmed_1: self.assertNotEqual(token, unconfirmed_1) else: unconfirmed_1 = token unconfirmed_count += 1 # since this tx came from outside our system there's no guarantee we # even see this since the block monitor might miss it before it's # actually confirmed if unconfirmed_count > 2: warnings.warn("got more than one unconfirmed notification for a single transaction")
async def test_always_get_unconfirmed_push_notification(self, *, ethminer, push_client): """Tests that when tx's are send through our systems we always get an unconfirmed push notification""" # register for GCM PNs body = { "registration_id": TEST_GCM_ID, "address": TEST_WALLET_ADDRESS } resp = await self.fetch_signed("/gcm/register", signing_key=TEST_ID_KEY, method="POST", body=body) self.assertResponseCodeEqual(resp, 204, resp.body) async with self.pool.acquire() as con: rows = await con.fetch("SELECT * FROM notification_registrations WHERE toshi_id = $1", TEST_ID_ADDRESS) self.assertIsNotNone(rows) self.assertEqual(len(rows), 1) # run this a bunch of times to see if # we can expose any race conditions for iteration in range(4): if iteration > 2: ethminer.pause() value = 2821181018869341261 resp = await self.fetch("/tx/skel", method="POST", body={ "from": FAUCET_ADDRESS, "to": TEST_WALLET_ADDRESS, "value": value }) self.assertResponseCodeEqual(resp, 200, resp.body) body = json_decode(resp.body) tx = sign_transaction(body['tx'], FAUCET_PRIVATE_KEY) resp = await self.fetch("/tx", method="POST", body={ "tx": tx }) self.assertResponseCodeEqual(resp, 200, resp.body) tx_hash = json_decode(resp.body)['tx_hash'] tx = decode_transaction(tx) self.assertEqual(tx_hash, data_encoder(tx.hash)) if iteration > 2: await asyncio.sleep(5) ethminer.start() async with self.pool.acquire() as con: rows = await con.fetch("SELECT * FROM transactions WHERE nonce = $1", tx.nonce) self.assertEqual(len(rows), 1) unconfirmed_count = 0 while True: token, payload = await push_client.get() self.assertEqual(token, TEST_GCM_ID) message = parse_sofa_message(payload['message']) self.assertIsInstance(message, SofaPayment) self.assertEqual(message['value'], hex(value)) self.assertEqual(message['txHash'], tx_hash) if message['status'] == "confirmed": break self.assertEqual(message['status'], "unconfirmed") unconfirmed_count += 1 # when the tx is sent through our systems we should # always get one unconfirmed notification, and we # should never get more than one self.assertEqual(unconfirmed_count, 1)