mirror of
https://github.com/FlipsideCrypto/web3.py.git
synced 2026-02-06 10:56:47 +00:00
264 lines
8.4 KiB
Python
Executable File
264 lines
8.4 KiB
Python
Executable File
import random
|
|
|
|
from ..rlp import encode, Serializable, decode
|
|
from ..rlp.sedes import big_endian_int, binary, Binary
|
|
from ..rlp.utils import int_to_big_endian
|
|
|
|
from ..eth_utils import (
|
|
decode_hex,
|
|
force_bytes,
|
|
coerce_args_to_bytes,
|
|
is_string,
|
|
to_canonical_address,
|
|
to_normalized_address,
|
|
pad_left,
|
|
keccak,
|
|
)
|
|
|
|
from .encoding import (
|
|
to_decimal,
|
|
decode_big_endian_int,
|
|
)
|
|
from .compat import (
|
|
Timeout,
|
|
)
|
|
|
|
|
|
def wait_for_transaction_receipt(web3, txn_hash, timeout=120):
|
|
with Timeout(timeout) as _timeout:
|
|
while True:
|
|
txn_receipt = web3.eth.getTransactionReceipt(txn_hash)
|
|
if txn_receipt is not None:
|
|
break
|
|
_timeout.sleep(random.random())
|
|
return txn_receipt
|
|
|
|
|
|
def get_block_gas_limit(web3, block_identifier=None):
|
|
if block_identifier is None:
|
|
block_identifier = web3.eth.blockNumber
|
|
block = web3.eth.getBlock(block_identifier)
|
|
return block['gasLimit']
|
|
|
|
|
|
def get_buffered_gas_estimate(web3, transaction, gas_buffer=100000):
|
|
gas_estimate_transaction = dict(**transaction)
|
|
|
|
gas_estimate = web3.eth.estimateGas(gas_estimate_transaction)
|
|
|
|
gas_limit = get_block_gas_limit(web3)
|
|
|
|
if gas_estimate > gas_limit:
|
|
raise ValueError(
|
|
"Contract does not appear to be delpoyable within the "
|
|
"current network gas limits. Estimated: {0}. Current gas "
|
|
"limit: {1}".format(gas_estimate, gas_limit)
|
|
)
|
|
|
|
return min(gas_limit, gas_estimate + gas_buffer)
|
|
|
|
|
|
def is_secp256k1_available():
|
|
try:
|
|
import secp256k1 # noqa
|
|
except ImportError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def is_bitcoin_available():
|
|
try:
|
|
import bitcoin # noqa
|
|
except ImportError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
# in the yellow paper it is specified that s should be smaller than secpk1n (eq.205)
|
|
secpk1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
|
|
TT256 = 2 ** 256
|
|
|
|
|
|
address_sedes = Binary.fixed_length(20, allow_empty=True)
|
|
|
|
|
|
class Transaction(Serializable):
|
|
"""
|
|
# Derived from `pyethereum.transaction.Transaction`
|
|
|
|
A transaction is stored as:
|
|
[nonce, gasprice, startgas, to, value, data, v, r, s]
|
|
|
|
nonce is the number of transactions already sent by that account, encoded
|
|
in binary form (eg. 0 -> '', 7 -> '\x07', 1000 -> '\x03\xd8').
|
|
|
|
(v,r,s) is the raw Electrum-style signature of the transaction without the
|
|
signature made with the private key corresponding to the sending account,
|
|
with 0 <= v <= 3. From an Electrum-style signature (65 bytes) it is
|
|
possible to extract the public key, and thereby the address, directly.
|
|
|
|
A valid transaction is one where:
|
|
(i) the signature is well-formed (ie. 0 <= v <= 3, 0 <= r < P, 0 <= s < N,
|
|
0 <= r < P - N if v >= 2), and
|
|
(ii) the sending account has enough funds to pay the fee and the value.
|
|
"""
|
|
|
|
fields = [
|
|
('nonce', big_endian_int),
|
|
('gasprice', big_endian_int),
|
|
('startgas', big_endian_int),
|
|
('to', address_sedes),
|
|
('value', big_endian_int),
|
|
('data', binary),
|
|
('v', big_endian_int),
|
|
('r', big_endian_int),
|
|
('s', big_endian_int),
|
|
]
|
|
|
|
_sender = None
|
|
|
|
def __init__(self, nonce, gasprice, startgas, to, value, data, v=0, r=0, s=0):
|
|
self.data = None
|
|
|
|
to = to_canonical_address(to)
|
|
assert len(to) == 20 or len(to) == 0
|
|
super(Transaction, self).__init__(nonce, gasprice, startgas, to, value, data, v, r, s)
|
|
|
|
if self.gasprice >= TT256 or self.startgas >= TT256 or \
|
|
self.value >= TT256 or self.nonce >= TT256:
|
|
raise ValueError("Values way too high!")
|
|
|
|
@property
|
|
def sender(self):
|
|
if not self._sender:
|
|
if not is_bitcoin_available() or not is_secp256k1_available():
|
|
raise ImportError(
|
|
"In order to derive the sender for transactions the "
|
|
"`bitcoin` and `secp256k1` packages must be installed."
|
|
)
|
|
from bitcoin import N
|
|
from secp256k1 import PublicKey, ALL_FLAGS
|
|
|
|
# Determine sender
|
|
if self.v:
|
|
has_invalid_signature_values = (
|
|
self.r >= N or
|
|
self.s >= N or
|
|
self.v < 27 or
|
|
self.v > 28 or
|
|
self.r == 0 or
|
|
self.s == 0
|
|
)
|
|
if has_invalid_signature_values:
|
|
raise ValueError("Invalid signature values!")
|
|
rlpdata = encode(self, UnsignedTransaction)
|
|
rawhash = keccak(rlpdata)
|
|
|
|
pk = PublicKey(flags=ALL_FLAGS)
|
|
try:
|
|
pk.public_key = pk.ecdsa_recover(
|
|
rawhash,
|
|
pk.ecdsa_recoverable_deserialize(
|
|
pad_left(
|
|
int_to_big_endian(self.r),
|
|
32,
|
|
b'\x00',
|
|
) + pad_left(
|
|
int_to_big_endian(self.s),
|
|
32,
|
|
b'\x00',
|
|
),
|
|
self.v - 27
|
|
),
|
|
raw=True
|
|
)
|
|
pub = pk.serialize(compressed=False)
|
|
except Exception:
|
|
raise ValueError("Invalid signature values (x^3+7 is non-residue)")
|
|
|
|
if pub[1:] == b"\x00" * (len(pub) - 1):
|
|
raise ValueError("Invalid signature (zero privkey cannot sign)")
|
|
|
|
self._sender = to_normalized_address(keccak(pub[1:])[-20:])
|
|
assert self.sender == self._sender
|
|
else:
|
|
self._sender = 0
|
|
return self._sender
|
|
|
|
@sender.setter
|
|
def sender(self, value):
|
|
self._sender = value
|
|
|
|
@coerce_args_to_bytes
|
|
def sign(self, key):
|
|
"""Sign this transaction with a private key.
|
|
|
|
A potentially already existing signature would be overridden.
|
|
"""
|
|
if not is_bitcoin_available() or not is_secp256k1_available():
|
|
raise ImportError(
|
|
"In order to sign transactions the "
|
|
"`bitcoin` and `secp256k1` packages must be installed."
|
|
)
|
|
from bitcoin import privtopub
|
|
from secp256k1 import PrivateKey
|
|
|
|
if key in (0, b'', b'\x00' * 32, b'0' * 64):
|
|
raise ValueError("Zero privkey cannot sign")
|
|
|
|
rawhash = keccak(encode(self, UnsignedTransaction))
|
|
|
|
if len(key) in {64, 66}:
|
|
# we need a binary key
|
|
key = decode_hex(key)
|
|
|
|
pk = PrivateKey(key, raw=True)
|
|
sig_bytes, rec_id = pk.ecdsa_recoverable_serialize(
|
|
pk.ecdsa_sign_recoverable(rawhash, raw=True)
|
|
)
|
|
signature = sig_bytes + force_bytes(chr(rec_id))
|
|
self.v = (ord(signature[64]) if is_string(signature[64]) else signature[64]) + 27
|
|
self.r = decode_big_endian_int(signature[0:32])
|
|
self.s = decode_big_endian_int(signature[32:64])
|
|
|
|
self.sender = to_normalized_address(keccak(privtopub(key)[1:])[-20:])
|
|
return self
|
|
|
|
|
|
UnsignedTransaction = Transaction.exclude(['v', 'r', 's'])
|
|
|
|
|
|
def serialize_transaction(transaction):
|
|
unsigned_transaction = UnsignedTransaction(
|
|
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']),
|
|
)
|
|
return encode(unsigned_transaction, UnsignedTransaction)
|
|
|
|
|
|
def add_signature_to_transaction(serialize_transaction, signature):
|
|
unsigned_transaction = decode(serialize_transaction, UnsignedTransaction)
|
|
|
|
v = (ord(signature[64]) if is_string(signature[64]) else signature[64]) + 27
|
|
r = decode_big_endian_int(signature[0:32])
|
|
s = decode_big_endian_int(signature[32:64])
|
|
|
|
signed_transaction = Transaction(
|
|
nonce=unsigned_transaction.nonce,
|
|
gasprice=unsigned_transaction.gasprice,
|
|
startgas=unsigned_transaction.startgas,
|
|
to=unsigned_transaction.to,
|
|
value=unsigned_transaction.value,
|
|
data=unsigned_transaction.data,
|
|
v=v,
|
|
r=r,
|
|
s=s,
|
|
)
|
|
return signed_transaction
|