Beispiel #1
0
    async def call(self, method, params=None, notification=False):

        msg = {"jsonrpc": "2.0", "method": method}
        if params:
            msg['params'] = params
        if not notification:
            msg['id'] = self.id
            self.id += 1
        self.con.write_message(tornado.escape.json_encode(msg))

        if notification:
            return

        future = asyncio.Future()
        tornado.ioloop.IOLoop.current().add_callback(self.handle_calls,
                                                     msg['id'], future)

        result = await future

        if 'error' in result:
            raise JsonRPCError(
                msg['id'], result['error']['code'], result['error']['message'],
                result['error']['data'] if 'data' in result['error'] else None)
        if 'result' not in result:
            raise JsonRPCError(
                msg['id'], -1,
                "missing result field from jsonrpc response: {}".format(
                    result), None)
        return result['result']
Beispiel #2
0
    async def cancel_queued_transaction(self, tx_hash, signature):

        if not validate_transaction_hash(tx_hash):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction_hash', 'message': 'Invalid Transaction Hash'})

        if not validate_signature(signature):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_signature', 'message': 'Invalid Signature'})

        async with self.db:
            tx = await self.db.fetchrow("SELECT * FROM transactions WHERE hash = $1 AND (status != 'error' OR status IS NULL)",
                                        tx_hash)
        if tx is None:
            raise JsonRPCError(None, -32000, "Transaction not found",
                               {'id': 'not_found', 'message': 'Transaction not found'})
        elif tx['status'] != 'queued' and tx['status'] is not None:
            raise JsonRPCError(None, -32000, "Transaction already sent to node",
                               {'id': 'invalid_transaction_status', 'message': 'Transaction already sent to node'})

        message = "Cancel transaction " + tx_hash
        if not personal_ecrecover(message, signature, tx['from_address']):
            raise JsonRPCError(None, -32000, "Permission Denied",
                               {'id': 'permission_denied', 'message': 'Permission Denied'})

        log.info("Setting tx '{}' to error due to user cancelation".format(tx['hash']))
        self.tasks.update_transaction(tx['transaction_id'], 'error')
Beispiel #3
0
    async def _execute_single(self,
                              data,
                              result_processor,
                              request_timeout=None):
        if request_timeout is None:
            request_timeout = self._request_timeout
        # NOTE: letting errors fall through here for now as it means
        # there is something drastically wrong with the jsonrpc server
        # which means something probably needs to be fixed
        req_start = time.time()
        retries = 0
        while True:
            try:
                resp = await self._httpclient.fetch(
                    self._url,
                    method="POST",
                    body=data,
                    request_timeout=request_timeout)
            except Exception as e:
                self.log.error(
                    "Error in JsonRPCClient._fetch ({}, {}) \"{}\" attempt {}".
                    format(data['method'], data['params'], str(e), retries))
                retries += 1
                if self.should_retry and isinstance(
                        e, HTTPError) and (e.status == 599 or e.status == 502):
                    # always retry after 599
                    pass
                elif not self.should_retry or time.time(
                ) - req_start >= request_timeout:
                    raise
                await asyncio.sleep(random.random())
                continue

            rval = await resp.json()

            # verify the id we got back is the same as what we passed
            if data['id'] != rval['id']:
                raise JsonRPCError(
                    -1, "returned id was not the same as the inital request")

            if "error" in rval:
                # handle potential issues with the block number requested being too high because
                # the nodes haven't all synced to the current block yet
                # TODO: this is only supported by parity: geth returns "<nil>" when the block number if too high
                if 'message' in rval['error'] and rval['error'][
                        'message'] == "Unknown block number":
                    retries += 1
                    if self.should_retry and time.time(
                    ) - req_start < request_timeout:
                        await asyncio.sleep(random.random())
                        continue
                raise JsonRPCError(
                    rval['id'], rval['error']['code'],
                    rval['error']['message'],
                    rval['error']['data'] if 'data' in rval['error'] else None)

            if result_processor:
                return result_processor(rval['result'])
            return rval['result']
    async def add_token(self, *, contract_address, name=None, symbol=None, decimals=None):

        if not self.user_toshi_id:
            raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': "Missing authorisation"})

        token = await self.get_token(contract_address)

        if token is None:
            raise JsonRPCError(None, -32000, "Invalid ERC20 Token",
                               {'id': 'bad_arguments', 'message': "Invalid ERC20 Token"})

        contract_address = token['contract_address']

        if 'balance' not in token:
            log.warning("didn't find a balance when adding custom token: {}".format(contract_address))
            balance = '0x0'
        else:
            balance = token['balance']

        async with self.db:
            await self.db.execute("INSERT INTO token_balances (eth_address, contract_address, name, symbol, decimals, balance, visibility) "
                                  "VALUES ($1, $2, $3, $4, $5, $6, $7) "
                                  "ON CONFLICT (eth_address, contract_address) DO UPDATE "
                                  "SET name = EXCLUDED.name, symbol = EXCLUDED.symbol, decimals = EXCLUDED.decimals, balance = EXCLUDED.balance, visibility = EXCLUDED.visibility",
                                  self.user_toshi_id, contract_address, name, symbol, decimals, balance, 2)
            await self.db.commit()

        return token
Beispiel #5
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()
    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)
        }
Beispiel #7
0
    async def execute(self):
        if not self._bulk_mode:
            raise Exception("No Bulk request started")
        if len(self._bulk_data) == 0:
            return []

        data = self._bulk_data[:]
        self._bulk_data = []
        futures = self._bulk_futures.copy()
        self._bulk_futures = {}
        req_start = time.time()

        retries = 0
        while True:
            try:
                resp = await self._httpclient.fetch(
                    self._url,
                    method="POST",
                    body=data,
                    request_timeout=
                    60.0  # higher request timeout than other operations
                )
            except Exception as e:
                self.log.error(
                    "Error in JsonRPCClient.execute: retry {}".format(retries))
                retries += 1
                if self.should_retry and isinstance(
                        e, HTTPError) and (e.status == 599 or e.status == 502):
                    # always retry after 599
                    pass
                elif not self.should_retry or time.time(
                ) - req_start >= self._request_timeout:
                    # give up after the request timeout
                    raise
                await asyncio.sleep(random.random())
                continue
            break

        rvals = await resp.json()

        results = []
        for rval in rvals:
            if 'id' not in rval:
                continue
            future, result_processor = futures.pop(rval['id'], (None, None))
            if future is None:
                self.log.warning("Got unexpected id in jsonrpc bulk response")
                continue
            if "error" in rval:
                future.set_exception(
                    JsonRPCError(
                        rval['id'], rval['error']['code'],
                        rval['error']['message'], rval['error']['data']
                        if 'data' in rval['error'] else None))
                result = None
            else:
                if result_processor:
                    result = result_processor(rval['result'])
                else:
                    result = rval['result']
                future.set_result(result)
            results.append(result)

        if len(futures):
            self.log.warning(
                "Found some unprocessed requests in bulk jsonrpc request")
            for future, result_processor in futures:
                future.set_exception(Exception("Unexpectedly missing result"))

        return results