web3.py/web3/providers/manager.py
2017-01-10 10:08:41 -07:00

285 lines
9.5 KiB
Python

import uuid
import json
import collections
import rlp
from web3.utils.crypto import sha3
from web3.utils.string import force_text
from web3.utils.address import to_address
from web3.utils.types import (
is_string,
is_object,
)
from web3.utils.encoding import (
to_decimal,
encode_hex,
decode_hex,
)
from web3.utils.transactions import (
is_bitcoin_available,
Transaction,
serialize_transaction,
add_signature_to_transaction,
)
from web3.utils.compat import (
spawn,
)
class RequestManager(object):
def __init__(self, provider):
self.pending_requests = {}
self.provider = provider
def setProvider(self, provider):
self.provider = provider
def request_blocking(self, method, params):
"""
Make a synchronous request using the provider
"""
response_raw = self.provider.make_request(method, params)
if is_string(response_raw):
response = json.loads(force_text(response_raw))
elif is_object(response_raw):
response = response_raw
if "error" in response:
raise ValueError(response["error"])
return response['result']
def request_async(self, method, params):
request_id = uuid.uuid4()
self.pending_requests[request_id] = spawn(
self.request_blocking,
method,
params,
)
return request_id
def receive_blocking(self, request_id, timeout=None):
try:
request = self.pending_requests.pop(request_id)
except KeyError:
raise KeyError("Request for id:{0} not found".format(request_id))
else:
response_raw = request.get(timeout=timeout)
response = json.loads(response_raw)
if "error" in response:
raise ValueError(response["error"])
return response['result']
def receive_async(self, request_id, *args, **kwargs):
raise NotImplementedError("Callback pattern not implemented")
class ManagerWrapper(object):
def __init__(self, wrapped_manager):
self.wrapped_manager = wrapped_manager
@property
def provider(self):
return self.wrapped_manager.provider
@property
def pending_requests(self):
return self.wrapped_manager.pending_requests
def setProvider(self, provider):
self.wrapped_manager.provider = provider
def request_blocking(self, *args, **kwargs):
return self.wrapped_manager.request_blocking(*args, **kwargs)
def request_async(self, *args, **kwargs):
return self.wrapped_manager.request_async(*args, **kwargs)
def receive_blocking(self, *args, **kwargs):
return self.wrapped_manager.receive_blocking(*args, **kwargs)
def receive_async(self, *args, **kwargs):
return self.wrapped_manager.receive_async(*args, **kwargs)
class BaseSendRawTransactionMixin(ManagerWrapper):
_known_transactions = None
_known_nonces = None
def __init__(self, *args, **kwargs):
self._known_transactions = collections.defaultdict(set)
self._known_nonces = collections.defaultdict(set)
super(BaseSendRawTransactionMixin, self).__init__(*args, **kwargs)
def _get_nonces_and_cleanup(self, addr, chain_nonce):
all_txns = {
txn_hash: self.request_blocking(
'eth_getTransactionByHash',
[txn_hash],
) for txn_hash in self._known_transactions[addr]
}
for txn_hash, txn in all_txns.items():
if txn is None:
continue
txn_nonce = to_decimal(txn['nonce'])
if txn_nonce < chain_nonce:
self._known_transactions[addr].discard(txn_hash)
else:
yield txn_nonce
all_known_nonces = tuple(self._known_nonces[addr])
for nonce in all_known_nonces:
if nonce < chain_nonce:
self._known_nonces[addr].discard(nonce)
else:
yield nonce
def get_chain_nonce(self, addr):
chain_nonce = to_decimal(self.request_blocking(
'eth_getTransactionCount',
[addr, 'pending']
))
return chain_nonce
def get_nonce(self, addr):
chain_nonce = self.get_chain_nonce(addr)
tracked_txn_nonces = tuple(self._get_nonces_and_cleanup(addr, chain_nonce))
nonce = max(0, chain_nonce, *tracked_txn_nonces)
if nonce == 0 and not tracked_txn_nonces:
return -1
else:
return nonce
def get_transaction_signature(self, serialized_txn):
raise NotImplementedError("Must be implemented by subclasses")
def sign_and_serialize_transaction(self, transaction):
serialized_txn = serialize_transaction(transaction)
signature = self.get_transaction_signature(transaction)
signed_transaction = add_signature_to_transaction(
serialized_txn,
signature,
)
signed_and_serialized_txn = rlp.encode(signed_transaction, Transaction)
return signed_and_serialized_txn
def construct_full_transaction(self, base_transaction):
txn_from = base_transaction['from']
full_txn = dict(**base_transaction)
full_txn.setdefault('nonce', self.get_nonce(txn_from) + 1)
full_txn.setdefault('gasPrice', self.request_blocking(
'eth_gasPrice', []
))
full_txn.setdefault('gas', hex(90000))
full_txn.setdefault('value', '0x0')
full_txn.setdefault('to', '')
full_txn.setdefault('data', '')
return full_txn
TXN_SENDING_METHODS = {
'eth_sendTransaction',
'eth_sendRawTransaction',
'personal_signAndSendTransaction',
'personal_sendTransaction',
}
def request_blocking(self, method, params):
if method == 'eth_sendTransaction':
base_transaction = params[0]
# create a fully signed transaction and send through the
# `eth_sendRawTransaction` endpoint instead.
full_transaction = self.construct_full_transaction(base_transaction)
raw_transaction_bytes = self.sign_and_serialize_transaction(
full_transaction,
)
raw_transaction_bytes_as_hex = encode_hex(raw_transaction_bytes)
return self.request_blocking(
'eth_sendRawTransaction', [raw_transaction_bytes_as_hex],
)
result = super(BaseSendRawTransactionMixin, self).request_blocking(
method, params,
)
if method in self.TXN_SENDING_METHODS:
if method == 'eth_sendRawTransaction':
txn = rlp.decode(decode_hex(params[0]), Transaction)
self._known_transactions[to_address(txn.sender)].add(result)
self._known_nonces[to_address(txn.sender)].add(txn.nonce)
else:
txn = params[0]
self._known_transactions[to_address(txn['from'])].add(result)
if 'nonce' in txn:
self._known_nonces[to_address(txn['from'])].add(
to_decimal(txn['nonce'])
)
return result
class DelegatedSigningManager(BaseSendRawTransactionMixin):
def __init__(self, *args, **kwargs):
self.signing_manager = kwargs.pop('signing_manager')
super(DelegatedSigningManager, self).__init__(*args, **kwargs)
def get_chain_nonce(self, addr):
signer_nonce = to_decimal(self.signing_manager.request_blocking(
'eth_getTransactionCount',
[addr, 'pending']
))
wrapped_nonce = to_decimal(self.wrapped_manager.request_blocking(
'eth_getTransactionCount',
[addr, 'pending']
))
return max(signer_nonce, wrapped_nonce)
def get_transaction_signature(self, transaction):
serialized_txn = serialize_transaction(transaction)
hash_to_sign = self.signing_manager.request_blocking(
'web3_sha3', [encode_hex(serialized_txn)],
)
signature_hex = self.signing_manager.request_blocking(
'eth_sign',
[
transaction['from'],
hash_to_sign,
],
)
signature = decode_hex(signature_hex)
return signature
class PrivateKeySigningManager(BaseSendRawTransactionMixin):
def __init__(self, *args, **kwargs):
if not is_bitcoin_available():
raise ImportError(
"In order to use the `PrivateKeySigningManager` the "
"`bitcoin` and `secp256k1` packages must be installed."
)
self.keys = kwargs.pop('keys', {})
super(PrivateKeySigningManager, self).__init__(*args, **kwargs)
def register_private_key(self, key):
from bitcoin import privtopub
address = to_address(sha3(privtopub(key)[1:])[-40:])
self.keys[address] = key
def sign_and_serialize_transaction(self, transaction):
txn_from = to_address(transaction['from'])
if txn_from not in self.keys:
raise KeyError("No signing key registered for from address: {0}".format(txn_from))
transaction = Transaction(
nonce=to_decimal(transaction['nonce']),
gasprice=to_decimal(transaction['gasPrice']),
startgas=to_decimal(transaction['gas']),
to=transaction['to'],
value=to_decimal(transaction['value']),
data=decode_hex(transaction['data']),
)
transaction.sign(self.keys[txn_from])
assert to_address(transaction.sender) == txn_from
return rlp.encode(transaction, Transaction)