Пример #1
0
    async def _run(self):
        while len(self._keys) > 0:
            try:
                # I'm worried here about attacks on the login endpoint that
                # would fill the keys list and block further login attempts
                # and/or use up all the memory on the server. This block
                # is in place to warn and debug issues should this end up
                # happening
                if len(self._keys) > 500:
                    grouping = len(self._keys) // 500
                    if grouping > self._posted_warning:
                        log.warning(
                            "Login keys list has reached {} keys".format(
                                len(self._keys)))
                        self._posted_warning = grouping
                    elif grouping < self._posted_warning:
                        log.warning(
                            "Login keys list has returned to {} keys".format(
                                len(self._keys)))
                        self._posted_warning = grouping
                elif self._posted_warning > 0:
                    log.warning(
                        "Login keys list length has returned to {} keys".
                        format(len(self._keys)))
                    self._posted_warning = 0

                allkeys = self._keys[:]
                for offset in range(0, len(allkeys), 500):
                    keys = allkeys[offset:offset + 500]
                    if len(keys) == 0:
                        continue
                    # the timeout here will cause <1 second latency on new login
                    # requests.
                    result = await get_redis_connection().blpop(
                        *keys, timeout=1, encoding='utf-8')
                    if result:
                        key, result = result
                        self._keys.remove(key)
                        keys.remove(key)
                        if key not in self._futures:
                            log.warning("got result for missing login key")
                        else:
                            future = self._futures.pop(key)
                            future.set_result(result)
                    # cleanup stale keys
                    for key in keys:
                        if key not in self._futures:
                            self._keys.remove(key)
                        future = self._futures[key]
                        if asyncio.get_event_loop().time(
                        ) - future._time > LOGIN_TOKEN_EXPIRY:
                            future.set_excetion(TimeoutError())
                        if future.done():
                            del self._futures[key]
                            self._keys.remove(key)
            except:
                log.exception("error while checking logins")
                if len(self._keys) > 0:
                    await asyncio.sleep(1)
        self._running = False
Пример #2
0
 async def _on_message(self, message):
     try:
         response = await WebsocketJsonRPCHandler(
             self.user_toshi_id, self.application, self)(message)
         if response:
             self.write_message(response)
     except:
         log.exception("unexpected error handling message: {}".format(message))
         raise
Пример #3
0
    async def _get_token_details(self, contract_address):
        """Get token details from the contract's metadata endpoints"""

        bulk = self.eth.bulk()
        balanceof_future = bulk.eth_call(to_address=contract_address, data="{}000000000000000000000000{}".format(
            ERC20_BALANCEOF_CALL_DATA, self.user_toshi_id[2:] if self.user_toshi_id is not None else "0000000000000000000000000000000000000000"))

        name_future = bulk.eth_call(to_address=contract_address, data=ERC20_NAME_CALL_DATA)
        sym_future = bulk.eth_call(to_address=contract_address, data=ERC20_SYMBOL_CALL_DATA)
        decimals_future = bulk.eth_call(to_address=contract_address, data=ERC20_DECIMALS_CALL_DATA)
        try:
            await bulk.execute()
        except:
            log.exception("failed getting token details")
            return None
        balance = data_decoder(unwrap_or(balanceof_future, "0x"))
        if balance and balance != "0x":
            try:
                balance = decode_abi(['uint256'], balance)[0]
            except:
                log.exception("Invalid erc20.balanceOf() result: {}".format(balance))
                balance = None
        else:
            balance = None
        name = data_decoder(unwrap_or(name_future, "0x"))
        if name and name != "0x":
            try:
                name = decode_abi(['string'], name)[0].decode('utf-8')
            except:
                log.exception("Invalid erc20.name() data: {}".format(name))
                name = None
        else:
            name = None
        symbol = data_decoder(unwrap_or(sym_future, "0x"))
        if symbol and symbol != "0x":
            try:
                symbol = decode_abi(['string'], symbol)[0].decode('utf-8')
            except:
                log.exception("Invalid erc20.symbol() data: {}".format(symbol))
                symbol = None
        else:
            symbol = None
        decimals = data_decoder(unwrap_or(decimals_future, "0x"))
        if decimals and decimals != "0x":
            try:
                decimals = decode_abi(['uint256'], decimals)[0]
            except:
                log.exception("Invalid erc20.decimals() data: {}".format(decimals))
                decimals = None
        else:
            decimals = None

        return balance, name, symbol, decimals
Пример #4
0
    async def post(self):

        if self.is_request_signed():
            sender_toshi_id = self.verify_request()
        else:
            # this is an anonymous transaction
            sender_toshi_id = None

        try:
            result = await ToshiEthJsonRPC(
                sender_toshi_id, self.application,
                self.request).send_transaction(**self.json)
        except JsonRPCInternalError as e:
            log.exception("Error in POST /tx from: {}: args: {}".format(
                sender_toshi_id, json_encode(self.json)))
            raise JSONHTTPError(500, body={'errors': [e.data]})
        except JsonRPCError as e:
            log.exception("Error in POST /tx from: {}: args: {}".format(
                sender_toshi_id, json_encode(self.json)))
            raise JSONHTTPError(400, body={'errors': [e.data]})
        except TypeError:
            log.exception("Error in POST /tx from {}: args: {}".format(
                sender_toshi_id, json_encode(self.json)))
            raise JSONHTTPError(400,
                                body={
                                    'errors': [{
                                        'id': 'bad_arguments',
                                        'message': 'Bad Arguments'
                                    }]
                                })

        self.write({"tx_hash": result})
Пример #5
0
    async def post(self):

        eth_address = self.verify_request()

        try:
            result = await ToshiEthJsonRPC(eth_address, self.application,
                                           self.request).add_token(**self.json)
        except JsonRPCInternalError as e:
            raise JSONHTTPError(500, body={'errors': [e.data]})
        except JsonRPCError as e:
            raise JSONHTTPError(400, body={'errors': [e.data]})
        except TypeError:
            log.exception("bad_arguments")
            raise JSONHTTPError(400,
                                body={
                                    'errors': [{
                                        'id': 'bad_arguments',
                                        'message': 'Bad Arguments'
                                    }]
                                })

        self.write(result)
Пример #6
0
    async def flush(self, endpoint, flush_delay_limit=10, max_size=50):

        last_flush = 0 # 0 so that the first event is always sent
        batch = []
        while True:
            batch.append(await self._queues[endpoint].get())
            while len(batch) < max_size and time.time() - flush_delay_limit < last_flush:
                try:
                    batch.append((await asyncio.wait_for(self._queues[endpoint].get(), 2)))
                except asyncio.TimeoutError:
                    break
            batch_json = '[{0}]'.format(','.join(batch))
            batch = []
            last_flush = time.time()

            data = {
                'data': base64.b64encode(batch_json.encode('utf8')),
                'verbose': 1,
                'ip': 0,
            }
            if self._api_key:
                data.update({'api_key': self._api_key})
            encoded_data = urllib.parse.urlencode(data).encode('utf8')

            resp = await to_asyncio_future(self._httpclient.fetch(
                self._endpoints[endpoint],
                method="POST",
                headers={'Content-Type': "application/x-www-form-urlencoded"},
                body=encoded_data))

            try:
                response = json_decode(resp.body)
                if response['status'] != 1:
                    log.error('Mixpanel error: {0}'.format(response['error']))
            except ValueError:
                log.exception('Cannot interpret Mixpanel server response: {0}'.format(resp.body))
Пример #7
0
 async def __aenter__(self):
     if self.connection is not None:
         raise DatabaseError("Connection already in progress")
     try:
         self.connection = await self.pool.acquire(timeout=self.timeout)
     except asyncpg.exceptions.ConnectionDoesNotExistError:
         log.exception("Error acquiring connection")
         # attempt to recover the database connection
         if self.pool is get_database_pool():
             set_database_pool(None)
             try:
                 await self.pool.close()
                 self.pool = await prepare_database(handle_migration=False)
                 self.connection = await self.pool.acquire(
                     timeout=self.timeout)
             except:
                 log.exception("Unable to recover global database pool")
                 # fail hard in the hope that restarting the system will fix things
                 sys.exit(1)
         else:
             raise
     self.transaction = self.connection.transaction()
     await self.transaction.start()
     return self
Пример #8
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):

            # disallow transaction overwriting for known transactions
            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')
            if existing:
                # debugging checks
                existing_tx = await self.eth.eth_getTransactionByHash(
                    existing['hash'])
                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)

            #log.info("Attempting to send transaction\nHash: {}\n{} -> {}\nValue: {} + {} (gas) * {} (startgas) = {}\nSender's Balance {} ({} unconfirmed)".format(
            #    calculate_transaction_hash(tx), from_address, to_address, tx.value, tx.startgas, tx.gasprice, tx.value + (tx.startgas * tx.gasprice), network_balance, balance))

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

            # validate the nonce
            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)

            # 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
Пример #9
0
    async def get_token_balances(self, eth_address, token_address=None):
        if not validate_address(eth_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_address',
                'message': 'Invalid Address'
            })
        if token_address is not None and not validate_address(token_address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_token_address',
                'message': 'Invalid Token Address'
            })

        # get token balances
        while True:
            async with self.db:
                result = await self.db.execute(
                    "UPDATE token_registrations SET last_queried = (now() AT TIME ZONE 'utc') WHERE eth_address = $1",
                    eth_address)
                await self.db.commit()
            registered = result == "UPDATE 1"

            if not registered:
                try:
                    async with RedisLock(
                            "token_balance_update:{}".format(eth_address)):
                        try:
                            await erc20_dispatcher.update_token_cache(
                                "*", eth_address)
                        except:
                            log.exception("Error updating token cache")
                            raise
                        async with self.db:
                            await self.db.execute(
                                "INSERT INTO token_registrations (eth_address) VALUES ($1)",
                                eth_address)
                            await self.db.commit()
                    break
                except RedisLockException:
                    # wait until the previous task is done and try again
                    await asyncio.sleep(0.1)
                    continue
            else:
                break

        if token_address:
            async with self.db:
                token = await self.db.fetchrow(
                    "SELECT symbol, name, decimals, format "
                    "FROM tokens WHERE contract_address = $1", token_address)
                if token is None:
                    return None
                balance = await self.db.fetchval(
                    "SELECT value "
                    "FROM token_balances "
                    "WHERE eth_address = $1 AND contract_address = $2",
                    eth_address, token_address)
            if balance is None:
                balance = "0x0"
            details = {
                "symbol": token['symbol'],
                "name": token['name'],
                "decimals": token['decimals'],
                "value": balance,
                "contract_address": token_address
            }
            if token['format'] is not None:
                details["icon"] = "{}://{}/token/{}.{}".format(
                    self.request.protocol, self.request.host, token_address,
                    token['format'])
            else:
                details['icon'] = None
            return details
        else:
            async with self.db:
                balances = await self.db.fetch(
                    "SELECT t.symbol, t.name, t.decimals, b.value, b.contract_address, t.format "
                    "FROM token_balances b "
                    "JOIN tokens t "
                    "ON t.contract_address = b.contract_address "
                    "WHERE eth_address = $1 ORDER BY t.symbol", eth_address)

            tokens = []
            for b in balances:
                details = {
                    "symbol": b['symbol'],
                    "name": b['name'],
                    "decimals": b['decimals'],
                    "value": b['value'],
                    "contract_address": b['contract_address']
                }
                if b['format'] is not None:
                    details["icon"] = "{}://{}/token/{}.{}".format(
                        self.request.protocol, self.request.host,
                        b['contract_address'], b['format'])
                else:
                    details['icon'] = None

                tokens.append(details)

            return tokens
Пример #10
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
        async with RedisLock("{}:{}".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 != 'error' or status is NULL)", from_address,
                    tx.nonce)

            # 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']))
                manager_dispatcher.update_transaction(
                    existing['transaction_id'], 'error')

            data = data_encoder(tx.data)
            if data and \
               ((data.startswith("0xa9059cbb") and len(data) == 138) or \
                (data.startswith("0x23b872dd") and len(data) == 202)):
                # check if the token is a known erc20 token
                async with self.db:
                    erc20_token = await self.db.fetchrow(
                        "SELECT * FROM tokens WHERE contract_address = $1",
                        to_address)
            else:
                erc20_token = False

            # add tx to database
            async with self.db:
                db_tx = await self.db.fetchrow(
                    "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) "
                    "RETURNING transaction_id", 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)

                if erc20_token:
                    token_value = int(data[-64:], 16)
                    if data.startswith("0x23b872dd"):
                        erc20_from_address = "0x" + data[34:74]
                        erc20_to_address = "0x" + data[98:138]
                    else:
                        erc20_from_address = from_address
                        erc20_to_address = "0x" + data[34:74]
                    await self.db.execute(
                        "INSERT INTO token_transactions "
                        "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value) "
                        "VALUES ($1, $2, $3, $4, $5, $6)",
                        db_tx['transaction_id'], 0,
                        erc20_token['contract_address'], erc20_from_address,
                        erc20_to_address, hex(token_value))

                await self.db.commit()

            # trigger processing the transaction queue
            manager_dispatcher.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
Пример #11
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):

        # strip begining and trailing whitespace from addresses
        if to_address is not None and isinstance(to_address, str):
            to_address = to_address.strip()
        if from_address is not None and isinstance(from_address, str):
            to_address = to_address.strip()
        if token_address is not None and isinstance(token_address, str):
            token_address = token_address.strip()

        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 balance 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)
                            # make sure the balance isn't negative
                            if value < 0:
                                raise JsonRPCInsufficientFundsError(
                                    data={
                                        'id': 'insufficient_funds',
                                        'message': 'Insufficient Funds'
                                    })
                            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 balance 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)
            })
        except binascii.Error as e:
            log.exception(
                "Error creating transaction skeleton: nonce:{} gasprice:{} startgas:{} to:{} value:{} data:{} network_id:{}"
                .format(nonce, gas_price, gas, to_address, value, data,
                        self.network_id))
            raise JsonRPCError(
                None, -32000, "Error creating transaction skeleton", {
                    'id': 'unexpected_error',
                    'message': "Error creating transaction skeleton"
                })

        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)
        }
Пример #12
0
    async def get_token(self, contract_address):

        if not validate_address(contract_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Contract Address'})
        if contract_address != contract_address.lower():
            if not checksum_validate_address(contract_address):
                raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Contract Address Checksum'})
            contract_address = contract_address.lower()

        async with self.db:
            row = await self.db.fetchrow(
                "SELECT symbol, name, contract_address, decimals, icon_url, format FROM tokens WHERE contract_address = $1",
                contract_address)

        if row:
            token = {
                'symbol': row['symbol'],
                'name': row['name'],
                'contract_address': row['contract_address'],
                'decimals': row['decimals']
            }
            if self.user_toshi_id:
                async with self.db:
                    balance = await self.db.fetchval(
                        "SELECT balance FROM token_balances WHERE contract_address = $1 and eth_address = $2",
                        contract_address, self.user_toshi_id)
                if balance is None:
                    try:
                        balance = await self.eth.eth_call(to_address=contract_address, data="{}000000000000000000000000{}".format(
                            ERC20_BALANCEOF_CALL_DATA, self.user_toshi_id[2:]))
                    except:
                        log.exception("Unable to get balance of erc20 token {} for address {}".format(contract_address,
                                                                                                      self.user_toshi_id))
                        return None
                    if balance == "0x":
                        balance = "0x0"
                    else:
                        # strip 0 padding
                        balance = hex(int(balance, 16))
                token['balance'] = balance
            if row['icon_url'] is not None:
                token['icon'] = row['icon_url']
            elif row['format'] is not None:
                token['icon'] = "{}://{}/token/{}.{}".format(self.request.protocol, self.request.host,
                                                             token['contract_address'], row['format'])
            else:
                token['icon'] = None
            return token

        balance, name, symbol, decimals = await self._get_token_details(contract_address)

        if balance is None:
            return None

        async with self.db:
            await self.db.execute("INSERT INTO tokens (contract_address, name, symbol, decimals, custom, ready) "
                                  "VALUES ($1, $2, $3, $4, $5, $6) "
                                  "ON CONFLICT (contract_address) DO NOTHING",
                                  contract_address, name, symbol, decimals, True, True)
            await self.db.commit()

        rval = {
            'symbol': symbol,
            'name': name,
            'contract_address': contract_address,
            'decimals': decimals
        }
        if self.user_toshi_id:
            rval['balance'] = hex(balance)
        return rval