예제 #1
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')
예제 #2
0
    async def get_collectibles(self, address, contract_address=None):

        if not validate_address(address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Address'})
        if contract_address is not None and not validate_address(contract_address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_contract_address', 'message': 'Invalid Contract Address'})

        if contract_address is None:
            async with self.db:
                collectibles = await self.db.fetch(COLLECTIBLE_UNION_QUERY, address)

            return {"collectibles": [{
                "contract_address": c['contract_address'],
                "value": hex(c['value']),
                "balance": hex(c['value']),
                "name": c['name'],
                "url": c["url"],
                "icon": c['icon']
            } for c in collectibles]}
        else:
            async with self.db:
                collectible = await self.db.fetchrow(
                    "SELECT * FROM collectibles WHERE contract_address = $1 AND ready = true",
                    contract_address)
                if collectible is None:
                    return None
                if collectible['type'] == 2:
                    token_results = await self.db.fetch(
                        "SELECT c.contract_address AS token_id, c.name, c.image, c.creator_address, c.description, c.token_uri, b.balance "
                        "FROM fungible_collectible_balances b "
                        "JOIN fungible_collectibles c ON b.contract_address = c.contract_address "
                        "WHERE c.collectible_address = $1 AND b.balance != '0x0' AND owner_address = $2",
                        contract_address, address)
                    tokens = []
                    for t in token_results:
                        token = dict(t)
                        if token['description'] is None:
                            token['description'] = "Created by: {}, balance: {}".format(token.pop('creator_address'), parse_int(token.pop('balance')))
                        tokens.append(token)
                else:
                    tokens = await self.db.fetch(
                        "SELECT token_id, name, image, description, token_uri FROM collectible_tokens "
                        "WHERE contract_address = $1 AND owner_address = $2",
                        contract_address, address)
                    tokens = [dict(t) for t in tokens]

            if collectible is None:
                return None
            return {
                "contract_address": collectible["contract_address"],
                "name": collectible["name"],
                "icon": collectible["icon"],
                "url": collectible["url"],
                "value": hex(len(tokens)),
                "balance": hex(len(tokens)),
                "tokens": tokens
            }
예제 #3
0
    async def get_collectibles(self, address, contract_address=None):

        if not validate_address(address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_address',
                'message': 'Invalid Address'
            })
        if contract_address is not None and not validate_address(
                contract_address):
            raise JsonRPCInvalidParamsError(
                data={
                    'id': 'invalid_contract_address',
                    'message': 'Invalid Contract Address'
                })

        if contract_address is None:
            async with self.db:
                collectibles = await self.db.fetch(
                    "SELECT t.contract_address, COUNT(t.token_id) AS value, c.name, c.icon, c.url "
                    "FROM collectible_tokens t "
                    "JOIN collectibles c ON c.contract_address = t.contract_address "
                    "WHERE t.owner_address = $1 AND c.ready = true "
                    "GROUP BY t.contract_address, c.name, c.icon, c.url",
                    address)

            return {
                "collectibles": [{
                    "contract_address": c['contract_address'],
                    "value": hex(c['value']),
                    "name": c['name'],
                    "url": c["url"],
                    "icon": c['icon']
                } for c in collectibles]
            }
        else:
            async with self.db:
                collectible = await self.db.fetchrow(
                    "SELECT * FROM collectibles WHERE contract_address = $1 AND ready = true",
                    contract_address)
                tokens = await self.db.fetch(
                    "SELECT * FROM collectible_tokens "
                    "WHERE contract_address = $1 AND owner_address = $2",
                    contract_address, address)

            if collectible is None:
                return None
            return {
                "contract_address": collectible["contract_address"],
                "name": collectible["name"],
                "icon": collectible["icon"],
                "url": collectible["url"],
                "value": hex(len(tokens)),
                "tokens": [dict(t) for t in tokens]
            }
예제 #4
0
    async def filter(self, *, address=None, topic=None):
        if address is not None:
            if not validate_address(address):
                raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Invalid Adddress'})
        if topic is not None:
            try:
                topic_id, topic = encode_topic(topic)
            except ValueError:
                raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Invalid Topic'})

        filter_id = await self.request_handler.filter(address, topic_id, topic)
        return filter_id
예제 #5
0
    async def subscribe(self, *addresses):
        if not addresses:
            raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Bad Arguments'})

        for address in addresses:
            if not validate_address(address):
                raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Bad Arguments'})

        try:
            await self.request_handler.subscribe(addresses)
        except:
            raise

        return True
예제 #6
0
    async def remove_token(self, *, contract_address):

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

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

        async with self.db:
            await self.db.execute("UPDATE token_balances SET visibility = 0 "
                                  "WHERE eth_address = $1 AND contract_address = $2",
                                  self.user_toshi_id, contract_address)
            await self.db.commit()
예제 #7
0
    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
예제 #8
0
    async def get_transaction_count(self, address):

        if not validate_address(address):
            raise JsonRPCInvalidParamsError(data={
                'id': 'invalid_address',
                'message': 'Invalid Address'
            })

        # get the network nonce
        nw_nonce = await self.eth.eth_getTransactionCount(address)

        # check the database for queued txs
        async with self.db:
            nonce = await self.db.fetchval(
                "SELECT nonce FROM transactions "
                "WHERE from_address = $1 "
                "AND (status is NULL OR status = 'queued' OR status = 'unconfirmed') "
                "ORDER BY nonce DESC", address)

        #nonce = nonce[0]['nonce'] if nonce else None
        if nonce is not None:
            # return the next usable nonce
            nonce = nonce + 1
            if nonce < nw_nonce:
                return nw_nonce
            return nonce
        else:
            return nw_nonce
예제 #9
0
 async def remove_filters(self, *filter_ids):
     for filter_id in filter_ids:
         if not validate_hex_string("0x" + filter_id):
             raise JsonRPCInvalidParamsError(data={
                 'id': 'bad_arguments',
                 'message': 'Bad Arguments'
             })
     await self.request_handler.remove_filters(filter_ids)
     return True
예제 #10
0
    async def get_balance(self, address):

        if not validate_address(address):
            raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Address'})

        confirmed, unconfirmed, _, _ = await self.get_balances(address)

        return {
            "confirmed_balance": hex(confirmed),
            "unconfirmed_balance": hex(unconfirmed)
        }
예제 #11
0
    async def get_transaction(self, tx_hash):

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

        tx = await self.eth.eth_getTransactionByHash(tx_hash)
        if tx is None:
            async with self.db:
                tx = await self.db.fetchrow(
                    "SELECT * FROM transactions WHERE "
                    "hash = $1 AND (status != 'error' OR status IS NULL) "
                    "ORDER BY transaction_id DESC",
                    tx_hash)
            if tx:
                tx = database_transaction_to_rlp_transaction(tx)
                tx = transaction_to_json(tx)
        return tx
예제 #12
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)
        }
예제 #13
0
    async def get_token_balances(self,
                                 eth_address,
                                 token_address=None,
                                 force_update=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
        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 or force_update:
            erc20_dispatcher.update_token_cache("*", eth_address)
            async with self.db:
                await self.db.execute(
                    "INSERT INTO token_registrations (eth_address) VALUES ($1) ON CONFLICT (eth_address) DO NOTHING",
                    eth_address)
                await self.db.commit()

        if token_address:
            async with self.db:
                token = await self.db.fetchrow(
                    "SELECT symbol, name, decimals, format, custom "
                    "FROM tokens WHERE contract_address = $1", token_address)
                if token is None:
                    return None
                if token['custom']:
                    custom_token = await self.db.fetchrow(
                        "SELECT name, symbol, decimals FROM token_balances "
                        "WHERE contract_address = $1 AND eth_address = $2",
                        token_address, eth_address)
                    if custom_token:
                        token = {
                            'name': custom_token['name'],
                            'symbol': custom_token['symbol'],
                            'decimals': custom_token['decimals'],
                            'format': token['format']
                        }
                balance = await self.db.fetchval(
                    "SELECT balance "
                    "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,  # NOTE: 'value' left in for backwards compatibility
                "balance": 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 COALESCE(b.symbol, t.symbol) AS symbol, COALESCE(b.name, t.name) AS name, COALESCE(b.decimals, t.decimals) AS decimals, b.balance, b.contract_address, t.format "
                    "FROM token_balances b "
                    "JOIN tokens t "
                    "ON t.contract_address = b.contract_address "
                    "WHERE b.eth_address = $1 AND "
                    "(b.visibility = 2 OR (b.visibility = 1 AND b.balance != '0x0')) "
                    "ORDER BY t.symbol", eth_address)

            tokens = []
            for b in balances:
                details = {
                    "symbol": b['symbol'],
                    "name": b['name'],
                    "decimals": b['decimals'],
                    "value":
                    b['balance'],  # NOTE: 'value' left in for backwards compatibility
                    "balance": b['balance'],
                    "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
예제 #14
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)}
예제 #15
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
예제 #16
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
예제 #17
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
예제 #18
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 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
예제 #19
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