async def test_get_balance_with_error_txs(self):

        tx1_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240c'
        tx2_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240d'
        tx3_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240e'
        addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb'
        val = 761751855997712

        await self.faucet(addr, val)

        async with self.pool.acquire() as con:
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7)", tx1_hash,
                FAUCET_ADDRESS, addr, 0, val, DEFAULT_STARTGAS,
                DEFAULT_GASPRICE)
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", tx2_hash,
                FAUCET_ADDRESS, addr, 1, val, DEFAULT_STARTGAS,
                DEFAULT_GASPRICE, 'unconfirmed')
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", tx3_hash,
                FAUCET_ADDRESS, addr, 2, val, DEFAULT_STARTGAS,
                DEFAULT_GASPRICE, 'error')

        resp = await self.fetch('/balance/{}'.format(addr))

        self.assertEqual(resp.code, 200)

        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val * 3)
Exemple #2
0
    async def test_create_and_send_transaction_with_max_value_with_pending_balance(
            self, *, monitor):

        await self.faucet(TEST_ADDRESS, 10 * 10**18)

        tx_hash_1 = await self.send_tx(TEST_PRIVATE_KEY, TEST_ADDRESS_2,
                                       5 * 10**18)

        resp = await self.fetch("/tx/skel",
                                method="POST",
                                body={
                                    "from": TEST_ADDRESS,
                                    "to": TEST_ADDRESS_2,
                                    "value": "max"
                                })
        self.assertEqual(resp.code, 200)
        body = json_decode(resp.body)
        tx_hash_2 = await self.sign_and_send_tx(TEST_PRIVATE_KEY, body['tx'])
        await self.wait_on_tx_confirmation(tx_hash_1)
        await self.wait_on_tx_confirmation(tx_hash_2)

        await monitor.block_check()
        await asyncio.sleep(0.1)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), 0)
        self.assertEqual(parse_int(data['unconfirmed_balance']), 0)
Exemple #3
0
    async def test_send_transaction_with_unconfirmed_funds(self, *, ethminer):
        """Tests that someone can create a tx with unconfirmed funds"""

        # make sure no blocks are mined
        ethminer.pause()

        # send funds to address1
        val1 = 10 ** 18
        resp = await self.fetch("/tx/skel", method="POST", body={
            "from": FAUCET_ADDRESS,
            "to": TEST_ADDRESS_1,
            "value": val1  # 1 eth
        })
        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)
        tx1_hash = json_decode(resp.body)['tx_hash']

        # send transaction from address1
        val2 = 2 * 10 ** 17  # 0.2 eth
        resp = await self.fetch("/tx/skel", method="POST", body={
            "from": TEST_ADDRESS_1,
            "to": TEST_ADDRESS_2,
            "value": val2
        })
        self.assertEqual(resp.code, 200)
        body = json_decode(resp.body)
        tx = sign_transaction(body['tx'], TEST_PRIVATE_KEY_1)
        resp = await self.fetch("/tx", method="POST", body={
            "tx": tx
        })
        self.assertEqual(resp.code, 200, resp.body)
        tx2_hash = json_decode(resp.body)['tx_hash']

        await asyncio.sleep(1)

        # make sure 2nd transaction is 'queued'
        async with self.pool.acquire() as con:
            row = await con.fetchrow("SELECT * FROM transactions WHERE from_address = $1", TEST_ADDRESS_1)
        self.assertIsNotNone(row)
        self.assertEqual(row['status'], 'queued')

        # make sure balance adjusts for queued items
        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS_1))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), 0)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val1 - (val2 + (DEFAULT_STARTGAS * DEFAULT_GASPRICE)))

        ethminer.start()

        await self.wait_on_tx_confirmation(tx1_hash)
        await self.wait_on_tx_confirmation(tx2_hash)

        await asyncio.sleep(1)
    async def test_create_and_send_transaction(self):

        val = 10 ** 10
        body = {
            "from": FAUCET_ADDRESS,
            "to": TEST_ADDRESS,
            "value": val
        }

        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)

        body = {
            "tx": tx
        }

        resp = await self.fetch("/tx", method="POST", body=body)

        self.assertEqual(resp.code, 200, resp.body)

        # ensure we get a tracking events
        self.assertEqual((await self.next_tracking_event())[0], None)

        body = json_decode(resp.body)
        tx_hash = body['tx_hash']

        tx = decode_transaction(tx)
        self.assertEqual(tx_hash, data_encoder(tx.hash))

        async with self.pool.acquire() as con:
            rows = await con.fetch("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
        self.assertEqual(len(rows), 1)

        # wait for a push notification
        await self.wait_on_tx_confirmation(tx_hash)
        while True:
            async with self.pool.acquire() as con:
                row = await con.fetchrow("SELECT * FROM transactions WHERE nonce = $1", tx.nonce)
            if row['status'] == 'confirmed':
                break

        # make sure updated field is updated
        self.assertGreater(row['updated'], row['created'])

        # make sure balance is returned correctly
        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val)
    async def test_get_balance_without_queued_items(self):

        tx0_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240b'
        tx1_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240c'
        tx2_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240d'
        tx3_hash = '0x2f321aa116146a9bc62b61c76508295f708f42d56340c9e613ebfc27e33f240e'
        addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb'
        addr2 = '0x66c3dcc38542467eb6ddeef194add1d9eaaf05e0'
        val = 76175185599771243
        sent_val = 2 * 10 ** 10

        await self.faucet(addr, val)

        async with self.pool.acquire() as con:
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
                tx0_hash, addr, addr2, 0, hex(sent_val),
                hex(DEFAULT_STARTGAS), hex(DEFAULT_GASPRICE), 'confirmed')
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7)",
                tx1_hash, addr, addr2, 0, hex(sent_val),
                hex(DEFAULT_STARTGAS), hex(DEFAULT_GASPRICE))
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
                tx2_hash, addr, addr2, 1, hex(sent_val),
                hex(DEFAULT_STARTGAS), hex(DEFAULT_GASPRICE),
                'queued')
            await con.execute(
                "INSERT INTO transactions (hash, from_address, to_address, nonce, value, gas, gas_price, status) "
                "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
                tx3_hash, addr, addr2, 2, hex(sent_val),
                hex(DEFAULT_STARTGAS), hex(DEFAULT_GASPRICE),
                'unconfirmed')

        resp = await self.fetch('/{}'.format(addr))

        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']),
                         val - (sent_val + (DEFAULT_STARTGAS * DEFAULT_GASPRICE)))

        resp = await self.fetch('/{}'.format(addr2))

        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), 0)
        self.assertEqual(parse_int(data['unconfirmed_balance']), sent_val)
    async def get_balances(self, eth_address, include_queued=True):
        """Gets the confirmed balance of the eth address from the ethereum network
        and adjusts the value based off any pending transactions.

        Returns 4 values as a tuple:
          - the confirmed (network) balance
          - the balance adjusted for any pending transactions
          - the total value of pending transactions sent from the given address
          - the total value of pending transactions sent to the given address
        """
        async with self.db:
            # get the last block number to use in ethereum calls
            # to avoid race conditions in transactions being confirmed
            # on the network before the block monitor sees and updates them in the database
            block = (
                await
                self.db.fetchval("SELECT blocknumber FROM last_blocknumber"))

            pending_sent = await self.db.fetch(
                "SELECT hash, value, gas, gas_price, status FROM transactions "
                "WHERE from_address = $1 "
                "AND ("
                "((status != 'error' AND status != 'confirmed') OR status = 'new') "
                "OR (status = 'confirmed' AND blocknumber > $2))", eth_address,
                block or 0)

            pending_received = await self.db.fetch(
                "SELECT hash, value, status FROM transactions "
                "WHERE to_address = $1 "
                "AND ("
                "((status != 'error' AND status != 'confirmed') OR status = 'new') "
                "OR (status = 'confirmed' AND blocknumber > $2))", eth_address,
                block or 0)

        pending_sent = sum(
            parse_int(p['value']) +
            (parse_int(p['gas']) * parse_int(p['gas_price']))
            for p in pending_sent
            if include_queued or p['status'] == 'unconfirmed')

        pending_received = sum(
            parse_int(p['value']) for p in pending_received
            if include_queued or p['status'] == 'unconfirmed')

        confirmed_balance = await self.eth.eth_getBalance(eth_address,
                                                          block=block
                                                          or "latest")

        balance = (confirmed_balance + pending_received) - pending_sent

        return confirmed_balance, balance, pending_sent, pending_received
Exemple #7
0
    async def _run_erc20_health_check(self):

        log.info("running erc20 health check")
        async with self.pool.acquire() as con:
            token_balances = await con.fetch("SELECT * FROM token_balances")

        bad = 0
        requests = []
        last_execute = 0
        bulk = self.eth.bulk()

        for token in token_balances:
            contract_address = token['contract_address']
            data = "0x70a08231000000000000000000000000" + token['eth_address'][
                2:]

            f = bulk.eth_call(to_address=contract_address, data=data)
            requests.append(
                (contract_address, token['eth_address'], f, token['value']))

            if len(requests) >= last_execute + 500:
                await bulk.execute()
                bulk = self.eth.bulk()
                last_execute = len(requests)

        if len(requests) > last_execute:
            await bulk.execute()

        bad_data = {}
        for contract_address, eth_address, f, db_value in requests:
            if not f.done():
                log.warning("future not done when checking erc20 cache")
                continue
            try:
                value = f.result()
            except:
                log.exception("error getting erc20 value {}:{}".format(
                    contract_address, eth_address))
                continue
            if parse_int(value) != parse_int(db_value):
                bad += 1
                bad_data.setdefault(eth_address, set()).add(contract_address)

        if bad > 0:
            log.warning(
                "Found {}/{} bad ERC20 caches over {} addresses".format(
                    bad, len(token_balances), len(bad_data)))

            for eth_address in bad_data:
                erc20_dispatcher.update_token_cache("*", eth_address)
                await asyncio.sleep(15)  # don't overload things
    async def test_get_balance(self):

        addr = '0x39bf9e501e61440b4b268d7b2e9aa2458dd201bb'
        val = 761751855997712

        await self.faucet(addr, val)

        resp = await self.fetch('/balance/{}'.format(addr))

        self.assertEqual(resp.code, 200)

        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val)
Exemple #9
0
 async def _eth_getLogs_with_block_number_validation(self, kwargs):
     req_start = time.time()
     from_block = parse_int(kwargs.get('fromBlock', None))
     to_block = parse_int(kwargs.get('toBlock', None))
     while True:
         bulk = self.bulk()
         bn_future = bulk.eth_blockNumber()
         lg_future = bulk._fetch("eth_getLogs", [kwargs])
         await bulk.execute()
         bn = bn_future.result()
         if (from_block and bn < from_block) or (to_block
                                                 and bn < to_block):
             if self.should_retry and time.time(
             ) - req_start < self._request_timeout:
                 await asyncio.sleep(random.random())
                 continue
             raise JsonRPCError(None, -32000, "Unknown block number", None)
         return lg_future.result()
Exemple #10
0
async def get_reports(request, conf, current_user):
    page = parse_int(request.args.get('page', None)) or 1
    if page < 1:
        page = 1
    limit = 10
    offset = (page - 1) * limit

    sql = ("SELECT * FROM reports "
           "ORDER BY report_id DESC "
           "OFFSET $1 LIMIT $2")
    args = [offset, limit]
    count_sql = ("SELECT COUNT(*) FROM reports")
    count_args = []
    async with conf.db.id.acquire() as con:
        rows = await con.fetch(sql, *args)
        count = await con.fetchrow(count_sql, *count_args)

    reports = []
    for row in rows:
        async with conf.db.id.acquire() as con:
            reporter = await con.fetchrow(
                "SELECT * FROM users WHERE toshi_id = $1",
                row['reporter_toshi_id'])
            reportee = await con.fetchrow(
                "SELECT * FROM users WHERE toshi_id = $1",
                row['reportee_toshi_id'])

        reporter = fix_avatar_for_user(conf.urls.id, dict(reporter))
        reportee = fix_avatar_for_user(conf.urls.id, dict(reportee))
        reports.append({
            'reporter': reporter,
            'reportee': reportee,
            'details': row['details'],
            'date': row['date']
        })

    total_pages = (count['count'] // limit) + (0 if count['count'] %
                                               limit == 0 else 1)

    def get_qargs(page=page, as_list=False, as_dict=False):
        qargs = {'page': page}
        if as_dict:
            return qargs
        if as_list:
            return qargs.items()
        return urlencode(qargs)

    return html(await env.get_template("reports.html").render_async(
        reports=reports,
        current_user=current_user,
        environment=conf.name,
        page="reports",
        total=count['count'],
        total_pages=total_pages,
        current_page=page,
        get_qargs=get_qargs))
    async def block_check(self):
        while not self._shutdown:
            block = await self.eth.eth_getBlockByNumber(self.last_block_number + 1)
            if block:
                if self._lastlog + 1800 < asyncio.get_event_loop().time():
                    self._lastlog = asyncio.get_event_loop().time()
                    log.info("Processing block {}".format(block['number']))

                if block['logsBloom'] != "0x" + ("0" * 512):
                    logs_list = await self.eth.eth_getLogs(fromBlock=block['number'],
                                                           toBlock=block['number'])
                    logs = {}
                    for _log in logs_list:
                        if _log['transactionHash'] not in logs:
                            logs[_log['transactionHash']] = [_log]
                        else:
                            logs[_log['transactionHash']].append(_log)
                else:
                    logs_list = []
                    logs = {}

                for tx in block['transactions']:
                    # send notifications to sender and reciever
                    if tx['hash'] in logs:
                        tx['logs'] = logs[tx['hash']]
                    await self.process_transaction(tx)

                if logs_list:
                    # send notifications for anyone registered
                    async with self.pool.acquire() as con:
                        for event in logs_list:
                            for topic in event['topics']:
                                filters = await con.fetch(
                                    "SELECT * FROM filter_registrations WHERE contract_address = $1 AND topic_id = $2",
                                    event['address'], topic)
                                for filter in filters:
                                    eth_dispatcher.send_filter_notification(
                                        filter['filter_id'], filter['topic'], event['data'])

                # update the latest block number, only if it is larger than the
                # current block number.
                block_number = parse_int(block['number'])
                if self.last_block_number < block_number:
                    self.last_block_number = block_number
                    async with self.pool.acquire() as con:
                        await con.execute("UPDATE last_blocknumber SET blocknumber = $1 "
                                          "WHERE blocknumber < $1",
                                          block_number)

                collectibles_dispatcher.notify_new_block(block_number)

            else:

                break

        self._block_checking_process = None
Exemple #12
0
async def get_txs(request, conf, user):
    page = parse_int(request.args.get('page', None)) or 1
    if page < 1:
        page = 1
    limit = 10
    offset = (page - 1) * limit
    where_clause = ''
    filters = [
        f for f in request.args.getlist('filter', [])
        if f in ['confirmed', 'unconfirmed', 'error']
    ]
    if filters:
        where_clause = "WHERE " + " OR ".join("status = '{}'".format(f)
                                              for f in filters)
        if 'unconfirmed' in filters:
            where_clause += " OR status IS NULL"
    async with conf.db.eth.acquire() as con:
        rows = await con.fetch(
            "SELECT * FROM transactions {} ORDER BY created DESC OFFSET $1 LIMIT $2"
            .format(where_clause), offset, limit)
        count = await con.fetchrow(
            "SELECT COUNT(*) FROM transactions {}".format(where_clause))
    txs = []
    for row in rows:
        tx = dict(row)
        tx['from_user'] = await get_toshi_user_from_payment_address(
            conf, tx['from_address'])
        tx['to_user'] = await get_toshi_user_from_payment_address(
            conf, tx['to_address'])
        txs.append(tx)

    total_pages = (count['count'] // limit) + (0 if count['count'] %
                                               limit == 0 else 1)

    def get_qargs(page=page, filters=filters, as_list=False, as_dict=False):
        qargs = {'page': page}
        if filters:
            qargs['filter'] = filters
        if as_dict:
            return qargs
        if as_list:
            return qargs.items()
        return urlencode(qargs, True)

    return html(await env.get_template("txs.html").render_async(
        txs=txs,
        current_user=user,
        environment=conf.name,
        page="txs",
        total=count['count'],
        total_pages=total_pages,
        current_page=page,
        active_filters=filters,
        get_qargs=get_qargs))
    async def update_token_cache(self, contract_address, *eth_addresses):

        if len(eth_addresses) == 0:
            return

        async with self.db:
            if contract_address == "*":
                tokens = await self.db.fetch(
                    "SELECT contract_address FROM tokens")
            else:
                tokens = [{'contract_address': contract_address}]

        futures = []
        for token in tokens:
            for address in eth_addresses:
                # data for `balanceOf(address)`
                data = "0x70a08231000000000000000000000000" + address[2:]
                f = asyncio.ensure_future(
                    self.eth.eth_call(to_address=token['contract_address'],
                                      data=data))
                futures.append((token['contract_address'], address, f))

        # wait for all the jsonrpc calls to finish
        await asyncio.gather(*[f[2] for f in futures], return_exceptions=True)
        bulk_update = []
        bulk_delete = []
        for contract_address, eth_address, f in futures:
            try:
                value = f.result()
                # value of "0x" means something failed with the contract call
                if value == "0x0000000000000000000000000000000000000000000000000000000000000000" or value == "0x":
                    if value == "0x":
                        log.warning(
                            "calling balanceOf for contract {} failed".format(
                                contract_address))
                    bulk_delete.append((contract_address, eth_address))
                else:
                    value = hex(
                        parse_int(value))  # remove hex padding of value
                    bulk_update.append((contract_address, eth_address, value))
            except:
                log.exception(
                    "WARNING: failed to update token cache of '{}' for address: {}"
                    .format(contract_address, eth_address))
                continue
        async with self.db:
            await self.db.executemany(
                "INSERT INTO token_balances (contract_address, eth_address, value) VALUES ($1, $2, $3) "
                "ON CONFLICT (contract_address, eth_address) DO UPDATE set value = EXCLUDED.value",
                bulk_update)
            await self.db.executemany(
                "DELETE FROM token_balances WHERE contract_address = $1 AND eth_address = $2",
                bulk_delete)
            await self.db.commit()
Exemple #14
0
def to_eth(wei, points=18):
    wei = str(parse_int(wei))
    pad = 18 - len(wei)
    if pad < 0:
        eth = wei[:abs(pad)] + "." + wei[abs(pad):abs(pad) + points]
    else:
        eth = "0." + wei.zfill(18)[:points]
    while eth.endswith("0"):
        eth = eth[:-1]
    if eth.endswith("."):
        eth += "0"
    return eth
    async def test_empty_account(self):
        """Makes sure an account can be emptied completely"""

        val = 10**16
        default_fees = DEFAULT_STARTGAS * DEFAULT_GASPRICE

        tx_hash = await self.send_tx(FAUCET_PRIVATE_KEY, TEST_ADDRESS, val)
        tx = await self.wait_on_tx_confirmation(tx_hash)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), val)
        self.assertEqual(parse_int(data['unconfirmed_balance']), val)

        resp = await self.fetch("/tx/skel",
                                method="POST",
                                body={
                                    "from": TEST_ADDRESS,
                                    "to": FAUCET_ADDRESS,
                                    "value": val - default_fees
                                })
        self.assertEqual(resp.code, 200)
        body = json_decode(resp.body)
        tx = sign_transaction(body['tx'], TEST_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']

        # wait for a push notification
        tx = await self.wait_on_tx_confirmation(tx_hash)

        # make sure balance is returned correctly (and is 0)
        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), 0)
        self.assertEqual(parse_int(data['unconfirmed_balance']), 0)
    async def test_create_and_send_transaction_with_max_value_to_contract(self, *, monitor, parity):

        contract = await Contract.from_source_code(
            SPLITTER_CONTRACT.encode('utf-8'), "Splitter",
            constructor_data=[[TEST_ADDRESS, TEST_ADDRESS_2, TEST_ADDRESS_3, TEST_ADDRESS_4]],
            deployer_private_key=FAUCET_PRIVATE_KEY)

        await self.faucet(TEST_ADDRESS, 9 * 10 ** 18)

        resp = await self.fetch("/tx/skel", method="POST", body={
            "from": TEST_ADDRESS,
            "to": contract.address,
            "value": "max"

        })
        self.assertEqual(resp.code, 200)
        body = json_decode(resp.body)
        tx_hash = await self.sign_and_send_tx(TEST_PRIVATE_KEY, body['tx'])
        await self.wait_on_tx_confirmation(tx_hash)

        await monitor.block_check()
        await asyncio.sleep(0.1)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), 0)
        self.assertEqual(parse_int(data['unconfirmed_balance']), 0)

        tx_hash = await contract.withdraw.set_sender(FAUCET_PRIVATE_KEY)()
        await monitor.block_check()
        await asyncio.sleep(0.1)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS_2))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        expected_balance = parse_int(data['confirmed_balance'])
        print(expected_balance)
        self.assertNotEqual(expected_balance, 0)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS_2))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), expected_balance)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS_3))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), expected_balance)

        resp = await self.fetch('/balance/{}'.format(TEST_ADDRESS_4))
        self.assertEqual(resp.code, 200)
        data = json_decode(resp.body)
        self.assertEqual(parse_int(data['confirmed_balance']), expected_balance)
Exemple #17
0
    async def _list_payment_updates(self, address, start_time, end_time=None):

        if end_time is None:
            end_time = datetime.utcnow()
        elif not isinstance(end_time, datetime):
            end_time = datetime.utcfromtimestamp(end_time)
        if not isinstance(start_time, datetime):
            start_time = datetime.utcfromtimestamp(start_time)

        async with self.db:
            txs = await self.db.fetch(
                "SELECT * FROM transactions WHERE "
                "(from_address = $1 OR to_address = $1) AND "
                "updated > $2 AND updated < $3"
                "ORDER BY transaction_id ASC", address, start_time, end_time)
        payments = []
        for tx in txs:
            status = tx['status']
            if status is None or status == 'queued':
                status = 'unconfirmed'
            value = parse_int(tx['value'])
            if value is None:
                value = 0
            else:
                value = hex(value)
            # if the tx was created before the start time, send the unconfirmed
            # message as well.
            if status == 'confirmed' and tx['created'] > start_time:
                payments.append(
                    SofaPayment(status='unconfirmed',
                                txHash=tx['hash'],
                                value=value,
                                fromAddress=tx['from_address'],
                                toAddress=tx['to_address'],
                                networkId=self.application.config['ethereum']
                                ['network_id']).render())
            payments.append(
                SofaPayment(status=status,
                            txHash=tx['hash'],
                            value=value,
                            fromAddress=tx['from_address'],
                            toAddress=tx['to_address'],
                            networkId=self.application.config['ethereum']
                            ['network_id']).render())

        return payments
def database_transaction_to_rlp_transaction(transaction):
    """returns an rlp transaction for the given transaction"""

    nonce = transaction['nonce']
    value = parse_int(transaction['value'])
    gas = parse_int(transaction['gas'])
    gas_price = parse_int(transaction['gas_price'])

    tx = create_transaction(nonce=nonce, gasprice=gas_price, startgas=gas,
                            to=transaction['to_address'], value=value,
                            data=data_decoder(transaction['data']),
                            v=parse_int(transaction['v']),
                            r=parse_int(transaction['r']),
                            s=parse_int(transaction['s']))

    return tx
Exemple #19
0
async def check_account_nonces(conf, dryrun=False):
    async with conf.db.id.acquire() as con:
        users = await con.fetch("SELECT toshi_id, payment_address FROM users")

    last_percent = -1
    async with conf.db.eth.acquire() as con:
        tr = con.transaction()
        await tr.start()
        for i, user in enumerate(users):
            percent = int((i / len(users)) * 100)
            if percent != last_percent:
                print("Progress: {}/{} ({}%)".format(i, len(users), percent))
                last_percent = percent
            from_address = user['payment_address']
            resp = await app.http.post(
                conf.urls.node,
                headers={'Content-Type': 'application/json'},
                data=json.dumps({
                    "jsonrpc": "2.0",
                    "id": random.randint(0, 1000000),
                    "method": "eth_getTransactionCount",
                    "params": [from_address]
                }).encode('utf-8'))
            if resp.status == 200:
                data = await resp.json()
                if 'result' in data and data['result'] is not None:
                    nonce = parse_int(data['result'])

                    res = await con.execute(
                        "UPDATE transactions SET last_status = 'error' WHERE from_address = $1 AND nonce >= $2 AND last_status != 'error'",
                        from_address, nonce)
                    if res != "UPDATE 0":
                        print("{}|{}: {}".format(user['toshi_id'],
                                                 from_address, res))

        if dryrun:
            await tr.rollback()
        else:
            await tr.commit()
    async def create_transaction_skeleton(self,
                                          *,
                                          to_address,
                                          from_address,
                                          value=0,
                                          nonce=None,
                                          gas=None,
                                          gas_price=None,
                                          data=None):

        if not validate_address(from_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_from_address',
                'message': 'Invalid From Address'
            })

        if to_address is not None and not validate_address(to_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_to_address',
                'message': 'Invalid To Address'
            })

        if value:
            value = parse_int(value)
            if value is None or value < 0:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_value',
                    'message': 'Invalid Value'
                })

        # check optional arguments

        if nonce is None:
            # check cache for nonce
            nonce = await self.get_transaction_count(from_address)
        else:
            nonce = parse_int(nonce)
            if nonce is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_nonce',
                    'message': 'Invalid Nonce'
                })

        if data is not None:
            if isinstance(data, int):
                data = hex(data)
            if isinstance(data, str):
                try:
                    data = data_decoder(data)
                except binascii.Error:
                    pass
            if not isinstance(data, bytes):
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_data',
                    'message': 'Invalid Data field'
                })
        else:
            data = b''

        if gas is None:
            try:
                gas = await self.eth.eth_estimateGas(from_address,
                                                     to_address,
                                                     data=data,
                                                     value=value)
            except JsonRPCError:
                # this can occur if sending a transaction to a contract that doesn't match a valid method
                # and the contract has no default method implemented
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_data',
                    'message': 'Invalid Data field'
                })
        else:
            gas = parse_int(gas)
            if gas is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_gas',
                    'message': 'Invalid Gas'
                })

        if gas_price is None:
            gas_price = self.application.config['ethereum'].getint(
                'default_gasprice', DEFAULT_GASPRICE)
        else:
            gas_price = parse_int(gas_price)
            if gas_price is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_gas_price',
                    'message': 'Invalid Gas Price'
                })

        try:
            tx = create_transaction(nonce=nonce,
                                    gasprice=gas_price,
                                    startgas=gas,
                                    to=to_address,
                                    value=value,
                                    data=data,
                                    network_id=self.network_id)
        except InvalidTransaction as e:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_transaction',
                'message': str(e)
            })

        if tx.intrinsic_gas_used > gas:
            raise JsonRPCInvalidParamsError(
                data={
                    'id':
                    'invalid_transaction',
                    'message':
                    'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'
                    .format(tx.intrinsic_gas_used, gas)
                })

        transaction = encode_transaction(tx)

        return transaction
 def network_id(self):
     return parse_int(self.application.config['ethereum']['network_id'])
Exemple #22
0
    async def get(self, address):

        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header('Access-Control-Allow-Methods', 'GET')

        offset = parse_int(self.get_argument('offset', '0'))
        limit = parse_int(self.get_argument('limit', '10'))
        status = set([s.lower() for s in self.get_arguments('status')])
        direction = set([d.lower() for d in self.get_arguments('direction')])
        order = self.get_argument('order', 'desc').upper()

        if not validate_address(address) or \
           offset is None or \
           limit is None or \
           (status and not status.issubset(['confirmed', 'unconfirmed', 'queued', 'error'])) or \
           (direction and not direction.issubset(['in', 'out'])) or \
           (order not in ['DESC', 'ASC']):
            raise JSONHTTPError(400,
                                body={
                                    'id': 'bad_arguments',
                                    'message': 'Bad Arguments'
                                })

        query = "SELECT * FROM transactions WHERE "
        args = [address, offset, limit]

        if len(direction) == 0 or len(direction) == 2:
            query += "(from_address = $1 OR to_address = $1) "
        elif 'in' in direction:
            query += "to_address = $1 "
        elif 'out' in direction:
            query += "from_address = $1 "

        if len(status) == 0:
            query += "AND (status != $4 OR status IS NULL) "
            args.append('error')
        else:
            status_query = []
            for s in status:
                if s == 'queued':
                    status_query.extend([
                        "status = ${}".format(len(args) + 1), "status IS NULL"
                    ])
                else:
                    status_query.append("status = ${}".format(len(args) + 1))
                args.append(s)
            query += "AND (" + " OR ".join(status_query) + ") "

        query += "ORDER BY created {} OFFSET $2 LIMIT $3".format(order)

        async with self.db:
            rows = await self.db.fetch(query, *args)

        transactions = []
        for row in rows:
            transactions.append({
                "hash":
                row['hash'],
                "to":
                row['to_address'],
                "from":
                row['from_address'],
                "nonce":
                hex(row['nonce']),
                "value":
                row['value'],
                "gas":
                row['gas'],
                "gas_price":
                row['gas_price'],
                "created_data":
                row['created'].isoformat(),
                "confirmed_data":
                row['updated'].isoformat() if row['blocknumber'] else None,
                "status":
                row['status'] if row['status'] is not None else 'queued',
                "data":
                row['data']
            })
        resp = {
            "transactions": transactions,
            "offset": offset,
            "limit": limit,
            "order": order
        }
        if len(direction) == 1:
            resp['direction'] = direction.pop()
        if status:
            resp['status'] = "&".join(status)
        self.write(resp)
Exemple #23
0
async def get_apps(request, conf, current_user):
    page = parse_int(request.args.get('page', None)) or 1
    if page < 1:
        page = 1
    limit = 10
    offset = (page - 1) * limit
    order_by = request.args.get('order_by', None)
    search_query = request.args.get('query', None)
    filter_by = request.args.get('filter', None)
    order = ('created', 'DESC')
    if order_by:
        if order_by in sortable_apps_columns:
            if order_by[0] == '-':
                order = (order_by[1:], 'ASC'
                         if order_by[1:] in negative_apps_columns else 'DESC')
            else:
                order = (order_by, 'DESC'
                         if order_by in negative_apps_columns else 'ASC')

    if search_query:
        # strip punctuation
        query = ''.join(
            [c for c in search_query if c not in string.punctuation])
        # split words and add in partial matching flags
        query = '|'.join(
            ['{}:*'.format(word) for word in query.split(' ') if word])
        args = [offset, limit, query]
        if order_by:
            query_order = "ORDER BY {} {}".format(*order)
        else:
            # default order by rank
            query_order = "ORDER BY TS_RANK_CD(t1.tsv, TO_TSQUERY($3)) DESC, name, username"
        sql = ("SELECT * FROM "
               "(SELECT * FROM users, TO_TSQUERY($3) AS q "
               "WHERE (tsv @@ q) AND is_app = true) AS t1 "
               "{} "
               "OFFSET $1 LIMIT $2".format(query_order))
        count_args = [query]
        count_sql = ("SELECT COUNT(*) FROM users, TO_TSQUERY($1) AS q "
                     "WHERE (tsv @@ q) AND is_app = true")
        async with conf.db.id.acquire() as con:
            rows = await con.fetch(sql, *args)
            count = await con.fetchrow(count_sql, *count_args)
    else:
        async with conf.db.id.acquire() as con:
            rows = await con.fetch(
                "SELECT * FROM users WHERE is_app = true ORDER BY {} {} NULLS LAST OFFSET $1 LIMIT $2"
                .format(*order), offset, limit)
            count = await con.fetchrow(
                "SELECT COUNT(*) FROM users WHERE is_app = true")

    apps = []
    for row in rows:
        app = fix_avatar_for_user(conf.urls.id, dict(row))
        apps.append(app)

    total_pages = (count['count'] // limit) + (0 if count['count'] %
                                               limit == 0 else 1)

    def get_qargs(page=page,
                  order_by=order_by,
                  query=search_query,
                  filter=filter_by,
                  as_list=False,
                  as_dict=False):
        qargs = {'page': page}
        if order_by:
            if order_by[0] == '+':
                order_by = order_by[1:]
            elif order_by[0] != '-':
                # toggle sort order
                print(order, order_by)
                if order[0] == order_by and order[1] == (
                        'ASC'
                        if order_by in negative_user_columns else 'DESC'):
                    order_by = '-{}'.format(order_by)
            qargs['order_by'] = order_by
        if query:
            qargs['query'] = query
        if filter:
            qargs['filter'] = filter
        if as_dict:
            return qargs
        if as_list:
            return qargs.items()
        return urlencode(qargs)

    return html(await env.get_template("apps.html").render_async(
        apps=apps,
        current_user=current_user,
        environment=conf.name,
        page="apps",
        total=count['count'],
        total_pages=total_pages,
        current_page=page,
        get_qargs=get_qargs))
    async def update_token_cache(self,
                                 contract_address,
                                 *eth_addresses,
                                 blocknumber=None):

        if len(eth_addresses) == 0:
            return

        is_wildcard = contract_address == "*"

        async with self.db:
            last_blocknumber = (
                await
                self.db.fetchval("SELECT blocknumber FROM last_blocknumber"))
            if blocknumber is None:
                blocknumber = last_blocknumber
            elif blocknumber > last_blocknumber:
                # don't continue until the block numbers match
                log.info(
                    "request to update erc20 cache before block processor is caught up"
                )
                erc20_dispatcher.update_token_cache(
                    contract_address, *eth_addresses,
                    blocknumber=blocknumber).delay(1)
                return
            if is_wildcard:
                tokens = await self.db.fetch(
                    "SELECT contract_address FROM tokens where custom = FALSE")
            else:
                tokens = [{'contract_address': contract_address}]

        if is_wildcard:
            if len(eth_addresses) > 1:
                # this is currently unneeded and dangerous
                raise Exception(
                    "wildcard update of token caches unsupported for multiple addresses"
                )
            log.info("START update_token_cache(\"*\", {})".format(
                eth_addresses[0]))
            start_time = time.time()
            # NOTE: we don't remove this at the end on purpose
            # to avoid spamming of "*" refreshes
            should_run = await self.redis.set(
                "bulk_token_update:{}".format(eth_addresses[0]),
                1,
                expire=60,
                exist=self.redis.SET_IF_NOT_EXIST)
            if not should_run:
                log.info("ABORT update_token_cache(\"*\", {}): {}".format(
                    eth_addresses[0], should_run))
                return

        client = self.eth.bulk()
        futures = []
        for eth_address in eth_addresses:
            for token in tokens:
                data = "0x70a08231000000000000000000000000" + eth_address[2:]
                f = client.eth_call(to_address=token['contract_address'],
                                    data=data,
                                    block=blocknumber)
                futures.append((token['contract_address'], eth_address, f))

        if len(futures) > 0:
            await client.execute()

            bulk_insert = []
            for token_contract_address, eth_address, f in futures:
                try:
                    value = f.result()
                    if value == "0x0000000000000000000000000000000000000000000000000000000000000000" or value == "0x":
                        if value == "0x":
                            log.warning(
                                "calling balanceOf for contract {} failed".
                                format(token_contract_address))
                        value = 0
                    else:
                        value = parse_int(value)  # remove hex padding of value
                    bulk_insert.append(
                        (token_contract_address, eth_address, hex(value)))
                except JsonRPCError as e:
                    if e.message == "Unknown Block Number":
                        # reschedule the update and abort for now
                        log.info(
                            "got unknown block number in erc20 cache update")
                        erc20_dispatcher.update_token_cache(
                            contract_address,
                            *eth_addresses,
                            blocknumber=blocknumber).delay(1)
                        return
                    log.exception(
                        "WARNING: failed to update token cache of '{}' for address: {}"
                        .format(token_contract_address, eth_address))

            send_update = False
            if len(bulk_insert) > 0:
                async with self.db:
                    await self.db.executemany(
                        "INSERT INTO token_balances (contract_address, eth_address, balance) "
                        "VALUES ($1, $2, $3) "
                        "ON CONFLICT (contract_address, eth_address) "
                        "DO UPDATE set balance = EXCLUDED.balance",
                        bulk_insert)
                    await self.db.commit()
                    send_update = True

            # wildcard updates usually mean we need to send a refresh trigger to clients
            # currently clients only use a TokenPayment as a trigger to refresh their
            # token cache, so we abuse this functionality here
            if is_wildcard and send_update:
                # lots of fake values so it doesn't get confused with a real tx
                data = {
                    "txHash":
                    "0x0000000000000000000000000000000000000000000000000000000000000000",
                    "fromAddress":
                    "0x0000000000000000000000000000000000000000",
                    "toAddress": eth_addresses[0],
                    "status": "confirmed",
                    "value": "0x0",
                    "contractAddress":
                    "0x0000000000000000000000000000000000000000"
                }
                message = "SOFA::TokenPayment: " + json_encode(data)
                manager_dispatcher.send_notification(eth_addresses[0], message)
        if is_wildcard:
            end_time = time.time()
            log.info("DONE update_token_cache(\"*\", {}) in {}s".format(
                eth_addresses[0], round(end_time - start_time, 2)))
Exemple #25
0
async def liveordev(request, conf, user):

    # get statistics

    async with conf.db.eth.acquire() as con:
        tx24h = await con.fetchrow(
            "SELECT COUNT(*) FROM transactions WHERE created > (now() AT TIME ZONE 'utc') - interval '24 hours'"
        )
        tx7d = await con.fetchrow(
            "SELECT COUNT(*) FROM transactions WHERE created > (now() AT TIME ZONE 'utc') - interval '7 days'"
        )
        tx1m = await con.fetchrow(
            "SELECT COUNT(*) FROM transactions WHERE created > (now() AT TIME ZONE 'utc') - interval '1 month'"
        )
        txtotal = await con.fetchrow("SELECT COUNT(*) FROM transactions")
        last_block = await con.fetchrow("SELECT * FROM last_blocknumber")

    async with conf.db.id.acquire() as con:
        u24h = await con.fetchrow(
            "SELECT COUNT(*) FROM users WHERE created > (now() AT TIME ZONE 'utc') - interval '24 hours'"
        )
        u7d = await con.fetchrow(
            "SELECT COUNT(*) FROM users WHERE created > (now() AT TIME ZONE 'utc') - interval '7 days'"
        )
        u1m = await con.fetchrow(
            "SELECT COUNT(*) FROM users WHERE created > (now() AT TIME ZONE 'utc') - interval '1 month'"
        )
        utotal = await con.fetchrow("SELECT COUNT(*) FROM users")

    users = {
        'day': u24h['count'],
        'week': u7d['count'],
        'month': u1m['count'],
        'total': utotal['count']
    }
    txs = {
        'day': tx24h['count'],
        'week': tx7d['count'],
        'month': tx1m['count'],
        'total': txtotal['count']
    }

    status = {}
    block = {'db': last_block['blocknumber']}
    # check service status
    # eth
    try:
        resp = await app.http.get('{}/v1/balance/0x{}'.format(
            conf.urls.eth, '0' * 40),
                                  timeout=SERVICE_CHECK_TIMEOUT)
        if resp.status == 200:
            status['eth'] = "OK"
        else:
            status['eth'] = "Error: {}".format(resp.status)
    except asyncio.TimeoutError:
        status['eth'] = "Error: timeout"
    # id
    try:
        resp = await app.http.get('{}/v1/user/0x{}'.format(
            conf.urls.id, '0' * 40),
                                  timeout=SERVICE_CHECK_TIMEOUT)
        if resp.status == 404:
            status['id'] = "OK"
        else:
            status['id'] = "Error: {}".format(resp.status)
    except asyncio.TimeoutError:
        status['id'] = "Error: timeout"
    # dir
    try:
        resp = await app.http.get('{}/v1/apps/'.format(conf.urls.dir),
                                  timeout=SERVICE_CHECK_TIMEOUT)
        if resp.status == 200:
            status['dir'] = "OK"
        else:
            status['dir'] = "Error: {}".format(resp.status)
    except asyncio.TimeoutError:
        status['dir'] = "Error: timeout"
    # rep
    try:
        resp = await app.http.get('{}/v1/timestamp'.format(conf.urls.rep),
                                  timeout=SERVICE_CHECK_TIMEOUT)
        if resp.status == 200:
            status['rep'] = "OK"
        else:
            status['rep'] = "Error: {}".format(resp.status)
    except asyncio.TimeoutError:
        status['rep'] = "Error: timeout"
    # node
    try:
        resp = await app.http.post(
            conf.urls.node,
            headers={'Content-Type': 'application/json'},
            data=json.dumps({
                "jsonrpc": "2.0",
                "id": random.randint(0, 1000000),
                "method": "eth_blockNumber",
                "params": []
            }).encode('utf-8'))
        if resp.status == 200:
            data = await resp.json()
            if 'result' in data:
                if data['result'] is not None:
                    status['node'] = "OK"
                    block['node'] = parse_int(data['result'])
            elif 'error' in data:
                status['node'] = data['error']
        else:
            status['node'] = "Error: {}".format(resp.status)
    except asyncio.TimeoutError:
        status['node'] = "Error: timeout"

    return html(await env.get_template("index.html").render_async(
        current_user=user,
        environment=conf.name,
        page="home",
        txs=txs,
        users=users,
        status=status,
        block=block))
Exemple #26
0
async def update_categories(request, conf, current_user):

    category_name = request.form.get('category', None)
    category_tag = request.form.get('tag', None)
    category_id = request.form.get('id', None)
    should_remove = request.form.get('remove', None)
    language = 'en'

    error = None

    if should_remove is not None:

        if category_id is None:
            error = "Missing category id"
        else:
            category_id = parse_int(category_id)

            async with conf.db.id.acquire() as con:
                await con.fetchval(
                    "DELETE FROM categories WHERE category_id = $1",
                    category_id)

    elif category_tag is None or category_name is None:
        error = "Missing name and tag"

    else:

        # force tags to be lowercase
        category_tag = category_tag.lower()

        if category_id is None:
            try:
                async with conf.db.id.acquire() as con:
                    category_id = await con.fetchval(
                        "INSERT INTO categories (tag) VALUES ($1) RETURNING category_id",
                        category_tag)
            except UniqueViolationError:
                error = "Tag already exists"

        # check again because we only update if the category_id was supplied by the user
        # or the insert statement above succeeded.
        if category_id is not None:

            category_id = parse_int(category_id)

            async with conf.db.id.acquire() as con:
                await con.execute(
                    "INSERT INTO category_names (category_id, name, language) VALUES ($1, $2, $3) "
                    "ON CONFLICT (category_id, language) DO UPDATE SET name = EXCLUDED.name",
                    category_id, category_name, language)

    get_sql = (
        "SELECT * FROM categories "
        "JOIN category_names ON categories.category_id = category_names.category_id AND category_names.language = $1"
        "ORDER BY categories.tag ASC ")

    async with conf.db.id.acquire() as con:
        rows = await con.fetch(get_sql, language)

    return html(await env.get_template("categories.html").render_async(
        categories=rows,
        error=error,
        current_user=current_user,
        environment=conf.name,
        page="categories"))
Exemple #27
0
 def network_id(self):
     return parse_int(config['ethereum']['network_id'])
Exemple #28
0
    async def create_transaction_skeleton(self, *, to_address, from_address, value=0, nonce=None, gas=None, gas_price=None, data=None):

        if not validate_address(from_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_from_address', 'message': 'Invalid From Address'})

        if to_address is not None and not validate_address(to_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_to_address', 'message': 'Invalid To Address'})

        if from_address != from_address.lower() and not checksum_validate_address(from_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_from_address', 'message': 'Invalid From Address Checksum'})

        if to_address is not None and to_address != to_address.lower() and not checksum_validate_address(to_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_to_address', 'message': 'Invalid To Address Checksum'})

        if value:
            value = parse_int(value)
            if value is None or value < 0:
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_value', 'message': 'Invalid Value'})

        # check optional arguments

        # check if we should ignore the given gasprice
        # NOTE: only meant to be here while cryptokitty fever is pushing
        # up gas prices... this shouldn't be perminant
        # anytime the nonce is also set, use the provided gas (this is to
        # support easier overwriting of transactions)
        if gas_price is not None and nonce is None:
            async with self.db:
                whitelisted = await self.db.fetchrow("SELECT 1 FROM from_address_gas_price_whitelist WHERE address = $1", from_address)
                if not whitelisted:
                    whitelisted = await self.db.fetchrow("SELECT 1 FROM to_address_gas_price_whitelist WHERE address = $1", to_address)
            if not whitelisted:
                gas_price = None

        if nonce is None:
            # check cache for nonce
            nonce = await self.get_transaction_count(from_address)
        else:
            nonce = parse_int(nonce)
            if nonce is None:
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_nonce', 'message': 'Invalid Nonce'})

        if data is not None:
            if isinstance(data, int):
                data = hex(data)
            if isinstance(data, str):
                try:
                    data = data_decoder(data)
                except binascii.Error:
                    pass
            if not isinstance(data, bytes):
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_data', 'message': 'Invalid Data field'})
        else:
            data = b''

        if gas is None:
            try:
                gas = await self.eth.eth_estimateGas(from_address, to_address, data=data, value=value)
            except JsonRPCError:
                # this can occur if sending a transaction to a contract that doesn't match a valid method
                # and the contract has no default method implemented
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_data', 'message': 'Unable to estimate gas for contract call'})
        else:
            gas = parse_int(gas)
            if gas is None:
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_gas', 'message': 'Invalid Gas'})

        if gas_price is None:
            # try and use cached gas station gas price
            gas_station_gas_price = self.redis.get('gas_station_standard_gas_price')
            if gas_station_gas_price:
                gas_price = parse_int(gas_station_gas_price)
            if gas_price is None:
                gas_price = self.application.config['ethereum'].getint('default_gasprice', DEFAULT_GASPRICE)
        else:
            gas_price = parse_int(gas_price)
            if gas_price is None:
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_gas_price', 'message': 'Invalid Gas Price'})

        try:
            tx = create_transaction(nonce=nonce, gasprice=gas_price, startgas=gas,
                                    to=to_address, value=value, data=data,
                                    network_id=self.network_id)
        except InvalidTransaction as e:
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction', 'message': str(e)})

        if tx.intrinsic_gas_used > gas:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_transaction',
                'message': 'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'.format(
                    tx.intrinsic_gas_used, gas)})

        transaction = encode_transaction(tx)

        return {"tx": transaction, "gas": hex(gas), "gas_price": hex(gas_price), "nonce": hex(nonce), "value": hex(value)}
Exemple #29
0
    async def send_transaction(self, *, tx, signature=None):

        try:
            tx = decode_transaction(tx)
        except:
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction', 'message': 'Invalid Transaction'})

        if is_transaction_signed(tx):

            tx_sig = data_encoder(signature_from_transaction(tx))

            if signature:

                if tx_sig != signature:

                    raise JsonRPCInvalidParamsError(data={
                        'id': 'invalid_signature',
                        'message': 'Invalid Signature: Signature in payload and signature of transaction do not match'
                    })
            else:

                signature = tx_sig
        else:

            if signature is None:
                raise JsonRPCInvalidParamsError(data={'id': 'missing_signature', 'message': 'Missing Signature'})

            if not validate_signature(signature):
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_signature',
                    'message': 'Invalid Signature: {}'.format(
                        'Invalid length' if len(signature) != 132 else 'Invalid hex value')
                })

            try:
                sig = data_decoder(signature)
            except Exception:
                log.exception("Unexpected error decoding valid signature: {}".format(signature))
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_signature',
                    'message': 'Invalid Signature'
                })

            add_signature_to_transaction(tx, sig)

        # validate network id, if it's not for "all networks"
        if tx.network_id is not None and self.network_id != tx.network_id:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_network_id',
                'message': 'Invalid Network ID'
            })

        from_address = data_encoder(tx.sender)
        to_address = data_encoder(tx.to)

        # prevent spamming of transactions with the same nonce from the same sender
        with RedisLock(self.redis, "{}:{}".format(from_address, tx.nonce),
                       raise_when_locked=partial(JsonRPCInvalidParamsError, data={'id': 'invalid_nonce', 'message': 'Nonce already used'}),
                       ex=5):

            # check for transaction overwriting
            async with self.db:
                existing = await self.db.fetchrow("SELECT * FROM transactions WHERE "
                                                  "from_address = $1 AND nonce = $2 AND status != $3",
                                                  from_address, tx.nonce, 'error')

            # disallow transaction overwriting when the gas is lower or the transaction is confirmed
            if existing and (parse_int(existing['gas_price']) >= tx.gasprice or existing['status'] == 'confirmed'):
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_nonce', 'message': 'Nonce already used'})

            # make sure the account has enough funds for the transaction
            network_balance, balance, _, _ = await self.get_balances(from_address)
            if existing:
                balance += parse_int(existing['value']) + parse_int(existing['gas']) * parse_int(existing['gas_price'])

            if balance < (tx.value + (tx.startgas * tx.gasprice)):
                raise JsonRPCInsufficientFundsError(data={'id': 'insufficient_funds', 'message': 'Insufficient Funds'})

            # validate the nonce (only necessary if tx doesn't already exist)
            if not existing:
                c_nonce = await self.get_transaction_count(from_address)

                if tx.nonce < c_nonce:
                    raise JsonRPCInvalidParamsError(data={'id': 'invalid_nonce', 'message': 'Provided nonce is too low'})
                if tx.nonce > c_nonce:
                    raise JsonRPCInvalidParamsError(data={'id': 'invalid_nonce', 'message': 'Provided nonce is too high'})

            if tx.intrinsic_gas_used > tx.startgas:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_transaction',
                    'message': 'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'.format(tx.intrinsic_gas_used, tx.startgas)})

            # now this tx fits enough of the criteria to allow it
            # onto the transaction queue
            tx_hash = calculate_transaction_hash(tx)

            if existing:
                log.info("Setting tx '{}' to error due to forced overwrite".format(existing['hash']))
                self.tasks.update_transaction(existing['transaction_id'], 'error')

            # add tx to database
            async with self.db:
                await self.db.execute(
                    "INSERT INTO transactions "
                    "(hash, from_address, to_address, nonce, "
                    "value, gas, gas_price, "
                    "data, v, r, s, "
                    "sender_toshi_id) "
                    "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
                    tx_hash, from_address, to_address, tx.nonce,
                    hex(tx.value), hex(tx.startgas), hex(tx.gasprice),
                    data_encoder(tx.data), hex(tx.v), hex(tx.r), hex(tx.s),
                    self.user_toshi_id)
                await self.db.commit()

            # trigger processing the transaction queue
            self.tasks.process_transaction_queue(from_address)
            # analytics
            # use notification registrations to try find toshi ids for users
            if self.user_toshi_id:
                sender_toshi_id = self.user_toshi_id
            else:
                async with self.db:
                    sender_toshi_id = await self.db.fetchval(
                        "SELECT toshi_id FROM notification_registrations WHERE "
                        "eth_address = $1",
                        from_address)
            async with self.db:
                receiver_toshi_id = await self.db.fetchval(
                    "SELECT toshi_id FROM notification_registrations WHERE "
                    "eth_address = $1",
                    to_address)
            self.track(sender_toshi_id, "Sent transaction")
            # it doesn't make sense to add user agent here as we
            # don't know the receiver's user agent
            self.track(receiver_toshi_id, "Received transaction", add_user_agent=False)

        return tx_hash
Exemple #30
0
    async def create_transaction_skeleton(self,
                                          *,
                                          to_address,
                                          from_address,
                                          value=0,
                                          nonce=None,
                                          gas=None,
                                          gas_price=None,
                                          data=None,
                                          token_address=None):

        if not validate_address(from_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_from_address',
                'message': 'Invalid From Address'
            })

        if to_address is not None and not validate_address(to_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_to_address',
                'message': 'Invalid To Address'
            })

        if from_address != from_address.lower(
        ) and not checksum_validate_address(from_address):
            raise JsonRPCInvalidParamsError(
                data={
                    'id': 'invalid_from_address',
                    'message': 'Invalid From Address Checksum'
                })

        if to_address is not None and to_address != to_address.lower(
        ) and not checksum_validate_address(to_address):
            raise JsonRPCInvalidParamsError(
                data={
                    'id': 'invalid_to_address',
                    'message': 'Invalid To Address Checksum'
                })

        # check if we should ignore the given gasprice
        # NOTE: only meant to be here while cryptokitty fever is pushing
        # up gas prices... this shouldn't be perminant
        # anytime the nonce is also set, use the provided gas (this is to
        # support easier overwriting of transactions)
        if gas_price is not None and nonce is None:
            async with self.db:
                whitelisted = await self.db.fetchrow(
                    "SELECT 1 FROM from_address_gas_price_whitelist WHERE address = $1",
                    from_address)
                if not whitelisted:
                    whitelisted = await self.db.fetchrow(
                        "SELECT 1 FROM to_address_gas_price_whitelist WHERE address = $1",
                        to_address)
            if not whitelisted:
                gas_price = None

        if gas_price is None:
            # try and use cached gas station gas price
            gas_station_gas_price = await self.redis.get(
                'gas_station_standard_gas_price')
            if gas_station_gas_price:
                gas_price = parse_int(gas_station_gas_price)
            if gas_price is None:
                gas_price = config['ethereum'].getint('default_gasprice',
                                                      DEFAULT_GASPRICE)
        else:
            gas_price = parse_int(gas_price)
            if gas_price is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_gas_price',
                    'message': 'Invalid Gas Price'
                })

        if gas is not None:
            gas = parse_int(gas)
            if gas is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_gas',
                    'message': 'Invalid Gas'
                })

        if nonce is None:
            # check cache for nonce
            nonce = await self.get_transaction_count(from_address)
        else:
            nonce = parse_int(nonce)
            if nonce is None:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_nonce',
                    'message': 'Invalid Nonce'
                })

        if data is not None:
            if isinstance(data, int):
                data = hex(data)
            if isinstance(data, str):
                try:
                    data = data_decoder(data)
                except binascii.Error:
                    pass
            if not isinstance(data, bytes):
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_data',
                    'message': 'Invalid Data field'
                })
        else:
            data = b''

        # flag to force arguments into an erc20 token transfer
        if token_address is not None:
            if not validate_address(token_address):
                raise JsonRPCInvalidParamsError(
                    data={
                        'id': 'invalid_token_address',
                        'message': 'Invalid Token Address'
                    })
            if data != b'':
                raise JsonRPCInvalidParamsError(
                    data={
                        'id': 'bad_arguments',
                        'message': 'Cannot include both data and token_address'
                    })

            if isinstance(value, str) and value.lower() == "max":
                # get the balance in the database
                async with self.db:
                    value = await self.db.fetchval(
                        "SELECT value FROM token_balances "
                        "WHERE contract_address = $1 AND eth_address = $2",
                        token_address, from_address)
                if value is None:
                    # get the value from the ethereum node
                    data = "0x70a08231000000000000000000000000" + from_address[
                        2:].lower()
                    value = await self.eth.eth_call(to_address=token_address,
                                                    data=data)

            value = parse_int(value)
            if value is None or value < 0:
                raise JsonRPCInvalidParamsError(data={
                    'id': 'invalid_value',
                    'message': 'Invalid Value'
                })
            data = data_decoder(
                "0xa9059cbb000000000000000000000000{}{:064x}".format(
                    to_address[2:].lower(), value))
            token_value = value
            value = 0
            to_address = token_address

        elif value:

            if value == "max":
                network_balance, balance, _, _ = await self.get_balances(
                    from_address)
                if gas is None:
                    code = await self.eth.eth_getCode(to_address)
                    if code:
                        # we might have to do some work
                        try:
                            gas = await self.eth.eth_estimateGas(from_address,
                                                                 to_address,
                                                                 data=data,
                                                                 value=0)
                        except JsonRPCError:
                            # no fallback function implemented in the contract means no ether can be sent to it
                            raise JsonRPCInvalidParamsError(
                                data={
                                    'id':
                                    'invalid_to_address',
                                    'message':
                                    'Cannot send payments to that address'
                                })
                        attempts = 0
                        # because the default function could do different things based on the eth sent, we make sure
                        # the value is suitable. if we get different values 3 times abort
                        while True:
                            if attempts > 2:
                                log.warning(
                                    "Hit max attempts trying to get max value to send to contract '{}'"
                                    .format(to_address))
                                raise JsonRPCInvalidParamsError(
                                    data={
                                        'id':
                                        'invalid_to_address',
                                        'message':
                                        'Cannot send payments to that address'
                                    })
                            value = balance - (gas_price * gas)
                            try:
                                gas_with_value = await self.eth.eth_estimateGas(
                                    from_address,
                                    to_address,
                                    data=data,
                                    value=value)
                            except JsonRPCError:
                                # no fallback function implemented in the contract means no ether can be sent to it
                                raise JsonRPCInvalidParamsError(
                                    data={
                                        'id':
                                        'invalid_to_address',
                                        'message':
                                        'Cannot send payments to that address'
                                    })
                            if gas_with_value != gas:
                                gas = gas_with_value
                                attempts += 1
                                continue
                            else:
                                break
                    else:
                        # normal address, 21000 gas per transaction
                        gas = 21000
                        value = balance - (gas_price * gas)
                else:
                    # preset gas, run with it!
                    value = balance - (gas_price * gas)
            else:
                value = parse_int(value)
                if value is None or value < 0:
                    raise JsonRPCInvalidParamsError(data={
                        'id': 'invalid_value',
                        'message': 'Invalid Value'
                    })

        if gas is None:
            try:
                gas = await self.eth.eth_estimateGas(from_address,
                                                     to_address,
                                                     data=data,
                                                     value=value)
            except JsonRPCError:
                # this can occur if sending a transaction to a contract that doesn't match a valid method
                # and the contract has no default method implemented.
                # this can also happen if the current state of the blockchain means that submitting the
                # transaction would fail (abort).
                if token_address is not None:
                    # when dealing with erc20, this usually means the user's balance for that token isn't
                    # high enough, check that and throw an error if it's the case, and if not fall
                    # back to the standard invalid_data error
                    async with self.db:
                        bal = await self.db.fetchval(
                            "SELECT value FROM token_balances "
                            "WHERE contract_address = $1 AND eth_address = $2",
                            token_address, from_address)
                    if bal is not None:
                        bal = parse_int(bal)
                        if bal < token_value:
                            raise JsonRPCInsufficientFundsError(
                                data={
                                    'id': 'insufficient_funds',
                                    'message': 'Insufficient Funds'
                                })
                raise JsonRPCInvalidParamsError(
                    data={
                        'id': 'invalid_data',
                        'message': 'Unable to estimate gas for contract call'
                    })
            # if data is present, buffer gas estimate by 20%
            if len(data) > 0:
                gas = int(gas * 1.2)

        try:
            tx = create_transaction(nonce=nonce,
                                    gasprice=gas_price,
                                    startgas=gas,
                                    to=to_address,
                                    value=value,
                                    data=data,
                                    network_id=self.network_id)
        except InvalidTransaction as e:
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_transaction',
                'message': str(e)
            })

        if tx.intrinsic_gas_used > gas:
            raise JsonRPCInvalidParamsError(
                data={
                    'id':
                    'invalid_transaction',
                    'message':
                    'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.'
                    .format(tx.intrinsic_gas_used, gas)
                })

        transaction = encode_transaction(tx)

        return {
            "tx": transaction,
            "gas": hex(gas),
            "gas_price": hex(gas_price),
            "nonce": hex(nonce),
            "value": hex(token_value) if token_address else hex(value)
        }