Sending a Web3 Ethereum Transaction

Updated: 2022-02-14

What is a Cryptographic ECDSA Signature?

An ECDSA (Elliptic Curve Digital Signature Algorithm) signature is generated by a private key in the elliptic curve family. We use these signatures as proof of identity (private key owner) and proof that the message has not been tampered with during the journey.

Signatures are effectively a user’s wax seal on an envelope. Signatures play a huge role at the low level in blockchain. All data must be signed before being transmitted to the blockchain.

Flowchart

Why use hardware wallets like our Zymbit modules?

Hardware wallets are built to securely store private keys on their hardware. Hardware wallets are more secure and harder to penetrate than locally saved private keys on computers, as computers have a lot more attack vectors through user access, malware, network, etc.

The private keys stored in our modules are never exportable and are always hidden even from the user.

Useful Resources:

Prerequisites

  • Zymbit Modules that support this feature:

  • Follow the Getting Started guide first, installing all baseline software.

  • All code snippets written in this article are written using python3. For more Zymbit API documentation (Python/C/C++) visit: HSM6 API Documentation

  • Python libraries used in this example:

    • This library is used for rlp encoding
    apt install pandoc
    
    • This library is used for Crypto.Hash() functions. In this example we will be using keccak hashes.
    pip3 install pycryptodome
    
    • Web3 for easy-to-use blockchain development.
    pip3 install web3
    

Setting up Infura project

The quickest way to interact with the ethereum blockchain network is to use remote providers like: Infura, Alchemy, or QuickNode. For this example, we will use Infura to interact with our web3 code.

  1. Create an Infura Account and log in.
  2. Create a new project this can be named anything you want.

Infura Login

  1. Go to project settings and change the endpoints dropdown from Mainnet->Ropsten. Ropsten is the test network we will be testing our transaction on. Write down the web link to our test project for web3 to communicate with.
Warning
If you are developing in Infura moving forward, make sure to not give out the project secret and project ID!

Infura Project

Web3 Example

Import the python libs we need:

  • Import zymkey to open up a zymkey instance, so we can communicate to our zymbit products for key generation and ecdsa signatures.
  • Import binascii for simple hexlify() and unhexlify() functions.
  • Per Ethereum standards, import RLP for data compression and Keccak for hashing.
  • Import Web3 for simple communication to Ethereum nodes.
#!/usr/bin/python3

import zymkey
import binascii
import rlp
from rlp.sedes import binary, Binary, big_endian_int, BigEndianInt, List, CountableList, boolean
from Crypto.Hash import keccak
from web3 import Web3

Open a Web3 instance:

Using the endpoint link from our Infura project, open a Web3 instance object.

# Create web3 instance, from infura project (node) connecting to Ropsten test network

w3 = Web3(Web3.HTTPProvider('https://ropsten.infura.io/v3/9f06183d0529494792242beb59be4ad3'))

Generating a secp256k1 key pair to be the sender:

Ethereum network is based off the Koblitz curve “secp256k1”, so individual accounts are generated from secp256k1 public/private key pairs. The private key in the key pair is used to sign/encrypt data to be sent to the network or other users.

The public key in the key pair is used to verify the data owner’s identity and decrypt the data received. The public key can not be reverse engineered to get the private key. It is safe to send out the public key to other users.

Warning
Never send another user a private key! The Zymbit hardware wallet never exposes the private key.

Generate a secp256k1 key pair and export the public key from the slot to use later.

# Ethereum is based on secp256k1 (Koblitz curve). Generate a "secp256k1" key pair using Zymbit API functions.

zymkey_pub_key_slot = zymkey.client.gen_key_pair("secp256k1")
pub_key = zymkey.client.get_public_key(zymkey_pub_key_slot)
print("zymkey secp256k1 public key:\n%s" % pub_key)

Generate the Ethereum public address/account from the secp256k1 public key

The next step covers generating an Ethereum public address. An Ethereum address is akin to a user’s account number on the Ethereum network and will be used to identify your transactions. Ethereum addresses look like this: 0x15C25E6EB5dE729d7e310d059e59659cCB86E6f6.

Details on how the Ethereum standard formulates a checksum address (public address) from a secp256k1 public key.

# Get ECDSA secp256k1 public key from zymkey and generate our Ethereum sender's checksum address
# Keccak Hash the public key and save only the last 20 bytes of the hash. This is to prevent reverse-engineering
# of the public address back to the original public key.

keccak_hash = keccak.new(digest_bits=256)
keccak_hash.update(pub_key)
keccak_digest = keccak_hash.hexdigest()
# Take the last 20 bytes
wallet_len = 40
wallet_addr = "0x" + keccak_digest[-wallet_len:]

We then need to randomize the capitalization of these values via EIP-55 standard.

# A built-in EIP-55 easy one-liner from Web3

checksum = Web3.toChecksumAddress(wallet_addr)
print("Eth Checksum:\n%s" % checksum)

--OR

#Performing EIP-55 ourselves

checksum = "0x"
# Remove '0x' from the address
address = wallet_addr[2:]
address_byte_array = address.encode("utf-8")
keccak_hash = keccak.new(digest_bits=256)
keccak_hash.update(address_byte_array)
keccak_digest = keccak_hash.hexdigest()
for i in range(len(address)):
    address_char = address[i]
    keccak_char = keccak_digest[i]
    if int(keccak_char, 16) >= 8:
        checksum += address_char.upper()
    else:
        checksum += str(address_char)
print("Eth Checksum:\n%s" % checksum)

Verify this is a valid ethereum public address using web3:

print("Valid checksum:\n%s" % Web3.isAddress(checksum))

This is the ethereum public address of our sender. The sender signs the transaction and sends it out. Once the receiver gets the transaction they will see it was from this sender’s address.

Notice

To properly send out transaction to the Ropsten test network with this sender’s address. We need funds in this address to pay for the fees required to send out this transaction.

Because Ropsten is a test network for development, we can fund ethereum addresses for free with test ether. (Note: There is only one address per day limit).

You can do this here: Ropsten Faucet

Creating the transaction classes and making them rlp.serializable()

RLP (Recursive Length Prefix) is a encoding method to compress binary and arrays of data. RLP is the official encoding method used on the ethereum network. So to abide by the standard we need to create a transaction class object, which have attributes that are rlp serializable. Thankfully the rlp library(pandoc) we installed earlier, lets us do this easily by subclassing rlp.serializable.

As of the london hard fork in august 2021, Ethereum Mainnet accepts multiple transaction types. There are currently three transaction types:

  • Transaction Legacy [Type 0]
  • Transaction EIP-2930 [Type 1]
  • Transaction EIP-1559 [Type 2]

All transactions are accepted by Mainnet and there will be more transaction types in the future. For this example, we will show generating and signing Transaction Legacy (it was used for a long period of time) and Transaction EIP-1559 (the current default transaction on Mainnet).

Create our rlp.serializable transaction classes (Order of attributes matter!):

#------------------------------------Abstract Type Declaractions------------------------------------------------------------------------------------
# This is a rlp serializable type for accessList param in Transaction EIP-2930 and EIP-1559

access_list_sede_type = CountableList(List([Binary.fixed_length(20, allow_empty=False), CountableList(BigEndianInt(32)),]),)
#------------------------------------Class Definitions----------------------------------------------------------------------------------------------
# Transaction Legacy
class TransactionLegacy(rlp.Serializable):
    fields = [
        ("nonce", big_endian_int),
        ("gasPrice", big_endian_int),
        ("gasLimit", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("v", big_endian_int),
        ("r", big_endian_int),
        ("s", big_endian_int),
    ]

# Transaction EIP-1559 
class RawTransactionType1559(rlp.Serializable):
    transaction_type = 2

    fields = [
        ("chainId", big_endian_int),
        ("nonce", big_endian_int),
        ("maxPriorityFeePerGas", big_endian_int),
        ("maxFeePerGas", big_endian_int),
        ("gas", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("accessList", access_list_sede_type),
    ]

class SignedTransactionType1559(rlp.Serializable):
    transaction_type = 2

    fields = [
        ("chainId", big_endian_int),
        ("nonce", big_endian_int),
        ("maxPriorityFeePerGas", big_endian_int),
        ("maxFeePerGas", big_endian_int),
        ("gas", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("accessList", access_list_sede_type),
        ("yParity", boolean),
        ("r", big_endian_int),
        ("s", big_endian_int),

    ]

Create the example transaction to send

Create an example transaction to send out on the Ropsten test network:

# grab nonce value of sender's account. nonce = number of transactions
nonce = w3.eth.getTransactionCount(checksum)
print("Nonce value:\n %i" % nonce)

# Example receiver's address
receiver_addr = '0x15C25E6EB5dE729d7e310d059e59659cCB86E6f6'

# Ropsten chain ID is 3
chain_id = 3

# prepare the transaction, chainID 3 is ropsten
transaction_legacy = TransactionLegacy(nonce = nonce, gasPrice = 500000, gasLimit = 800000, to = binascii.unhexlify(receiver_addr.replace('0x', '')), value = 5, data = b'hello', v = chain_id, r = 0, s = 0)
transaction_1559 = RawTransactionType1559(chainId = chain_id, nonce = nonce, maxPriorityFeePerGas = 150000, maxFeePerGas = 150000, gas = 210000,
                                          to = binascii.unhexlify(receiver_addr.replace('0x', '')), value = 10, data = b'world', accessList = [])

Preparing our transaction to be signed by the Zymbit hardware

So the two steps to prepare our transaction for signing:

  1. RLP encode the transaction
  2. Keccak Hash the RLP encoded transaction

Per Ethereum standard the signature elements are generated for these transaction types via:

  • Transaction Legacy
    • The signature_r, signature_s elements of this transaction represent a secp256k1 signature over keccak256(rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]))
    • r,s are defaulted to 0
    • v will be the chain_id of the network
    • signature_v follows EIP-155
  • Transaction EIP-1559
    • The signature_y_parity, signature_r, signature_s elements of this transaction represent a secp256k1 signature over keccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list]))
    • chain_id and y_parity are now seperate params.

And because our transaction classes subclass rlp.serializable, we can simply rlp encode these class objects via:

# RLP encode the transaction

encoded_transaction_legacy = rlp.encode(transaction_legacy)
print("encoded transaction:\n%s" % binascii.hexlify(encoded_transaction_legacy).decode("utf-8"))

# Transaction type is now a byte added to the front of the rlp encoded object
encoded_transaction_1559 = bytes([2]) + rlp.encode(transaction_1559)
print("encoded transaction:\n%s" % binascii.hexlify(encoded_transaction_1559).decode("utf-8")) 

Next, step 2 is to keccak hash the rlp encoded transaction

#Per Ethereum standards, Keccak hash rlp encoded transaction
keccak_hash=keccak.new(digest_bits=256)
keccak_hash.update(encoded_transaction)
print("keccak_hash:\n%s" % keccak_hash.hexdigest())

Signing the transaction data with a private key stored on the Zymbit module.

Sign this keccak hashed transaction with the private secp256k1 key we generated. This ECDSA signature we get will give us signature_r, signature_s, signature_v, and the y-parity.

signature_V is only required by TransactionLegacy and follow EIP-155.

  • The other transaction types split up chain ID and Y parity as separate parameters.
# sign the transaction hash and calculate v, r, s values
signature, y_parity = zymkey.client.sign_digest(keccak_hash, zymkey_pub_key_slot, return_recid=True)
print ("ECDSA Signature:\n%s" % signature)
print("ECDSA Sig Length:\n%s" % len(signature))

# Verify the ECDSA signature. Make sure our public key can decrypt this signature.
verify_sig = zymkey.client.verify_digest(keccak_hash, signature, True, zymkey_pub_key_slot, False)
print("Verify sig:\n%s" % verify_sig)

# Signature consists of a R, S, V
# R is the first half of the signature converted to int
# S is the second half of the signature converted to int
# From EIP 155, V = chainId * 2 + 35 + recovery_id of public key
# Per Ethereum yellow paper, if S * 2 >= N, then S becomes N - S and Y-parity is flipped

N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
r = int.from_bytes(signature[:32], "big")
s = int.from_bytes(signature[-32:], "big")

y_parity = bool(y_parity.value)
if((s*2) >= N):
   y_parity = not y_parity
   s = N - s

# EIP-155 V is only required in TransactionLegacy, as other transaction types split up chain_id and y_parity.
v = chain_id * 2 + 35 + int(y_parity)

print ("R:\n%s" % r)
print ("S:\n%s" % s)
print ("V:\n%s" % v)
print ("Y Parity:\n%s" % y_parity)

Adding our signature to our transaction class

Transaction Legacy’s signature elements are:

  • Signature_R
  • Signature_S
  • Signature_V
# We create our TransactionLegacy again, except we fill our v, r, s fields with the appropriate values.

signed_transaction_legacy = TransactionLegacy(transaction_legacy.nonce, transaction_legacy.gasPrice, transaction_legacy.gasLimit, transaction_legacy.to, transaction_legacy.value, transaction_legacy.data, v, r, s)

Transaction EIP-1559’s signature elements are:

  • Signature_R
  • Signature_S
  • Y-Parity of the Signature
# We create our SignedTransactionType1559 from RawTransactionType1559, except we fill our yParity, r, s fields with the appropriate values.

signed_transaction_1559 = SignedTransactionType1559(transaction_1559.chainId, transaction_1559.nonce, transaction_1559.maxPriorityFeePerGas, transaction_1559.maxFeePerGas, transaction_1559.gas,
                                               transaction_1559.to, transaction_1559.value, transaction_1559.data, transaction_1559.accessList, y_parity, r, s)

One Last Step before send off. RLP encode the signed transaction.

Same as the rlp encoding step before.

# RLP encode the transaction

encoded_transaction_legacy = rlp.encode(signed_transaction_legacy)
print("encoded transaction:\n%s" % binascii.hexlify(encoded_transaction_legacy).decode("utf-8"))

# Transaction type is now a byte added to the front of the rlp encoded object
encoded_transaction_1559 = bytes([2]) + rlp.encode(signed_transaction_1559)
print("encoded transaction:\n%s" % binascii.hexlify(encoded_transaction_1559).decode("utf-8")) 

We can also validate that this transaction can be broadcasted, by pasting that encoded transaction in the myCrypto broadcast tool.

image

Send your transaction

Broadcast the transaction through our web3 instance.

# send raw transaction
transaction_result_hash = w3.eth.sendRawTransaction(encoded_signed_transaction)
print("Transaction broadcast hash:\n%s" % binascii.hexlify(transaction_result_hash).decode("utf-8"))

If successfully broadcasted, you should get a broadcast hash receipt back. You can view your transaction’s validation progress on the network by pasting that hash receipt here: https://ropsten.etherscan.io/

image

Warning

If you get Error: “Not enough funds or gas”

To properly send out transaction to the Ropsten test network with this sender’s address, there must be adequate funds in this address to pay for the fees required to send out this transaction. Because Ropsten is a test network for development, we can fund ethereum addresses for free with test ether. There is a one IP address per day limit).

You can do this here: Ropsten Faucet

Full Example Code

Below is the entire code used in the above examples.

#!/usr/bin/python3

import zymkey
import binascii
import rlp
from rlp.sedes import binary, Binary, big_endian_int, BigEndianInt, List, CountableList, boolean
from Crypto.Hash import keccak
from web3 import Web3

#------------------------------------Abstract Type Declaractions------------------------------------------------------------------------------------
# This is a rlp serializable type for accessList param
access_list_sede_type = CountableList(List([Binary.fixed_length(20, allow_empty=False), CountableList(BigEndianInt(32)),]),)
#------------------------------------Class Definitions----------------------------------------------------------------------------------------------

class TransactionLegacy(rlp.Serializable):
    fields = [
        ("nonce", big_endian_int),
        ("gasPrice", big_endian_int),
        ("gasLimit", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("v", big_endian_int),
        ("r", big_endian_int),
        ("s", big_endian_int),
    ]

class RawTransactionType1559(rlp.Serializable):
    transaction_type = 2

    fields = [
        ("chainId", big_endian_int),
        ("nonce", big_endian_int),
        ("maxPriorityFeePerGas", big_endian_int),
        ("maxFeePerGas", big_endian_int),
        ("gas", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("accessList", access_list_sede_type),
    ]

class SignedTransactionType1559(rlp.Serializable):
    transaction_type = 2

    fields = [
        ("chainId", big_endian_int),
        ("nonce", big_endian_int),
        ("maxPriorityFeePerGas", big_endian_int),
        ("maxFeePerGas", big_endian_int),
        ("gas", big_endian_int),
        ("to", Binary.fixed_length(20, allow_empty=True)),
        ("value", big_endian_int),
        ("data", binary),
        ("accessList", access_list_sede_type),
        ("yParity", boolean),
        ("r", big_endian_int),
        ("s", big_endian_int),

    ]
#------------------------------------------------------------------------------------------------------------------------------------------------------
# create web3 instance, from infura project (node) connecting to ropsten test network
w3 = Web3(Web3.HTTPProvider('https://ropsten.infura.io/v3/9f06183d0529494792242beb59be4ad3'))

# Example receiver's address
receiver_addr = '0x15C25E6EB5dE729d7e310d059e59659cCB86E6f6'

# Ethereum is based on secp256k1, so we generate a "secp256k1" key pair using zymbit libs.
# Get ECDSA secp256k1 public key from zymkey and generate our Ethereum sender's checksum address
zymkey_pub_key_slot = zymkey.client.gen_key_pair("secp256k1")
pub_key = zymkey.client.get_public_key(zymkey_pub_key_slot)
print("zymkey secp256k1 public key:\n%s" % pub_key)

keccak_hash = keccak.new(digest_bits=256)
keccak_hash.update(pub_key)
keccak_digest = keccak_hash.hexdigest()
# Take the last 20 bytes
wallet_len = 40
wallet_addr = "0x" + keccak_digest[-wallet_len:]

# checksum = Web3.toChecksumAddress(wallet_addr)

# How eip-55 calculates checksum
checksum = "0x"
# Remove ‘0x’ from the address
address = wallet_addr[2:]
address_byte_array = address.encode("utf-8")
keccak_hash = keccak.new(digest_bits=256)
keccak_hash.update(address_byte_array)
keccak_digest = keccak_hash.hexdigest()
for i in range(len(address)):
    address_char = address[i]
    keccak_char = keccak_digest[i]
    if int(keccak_char, 16) >= 8:
        checksum += address_char.upper()
    else:
        checksum += str(address_char)

print("Eth Checksum:\n%s" % checksum)

print("Valid checksum:\n%s" % Web3.isAddress(checksum))

# grab nonce value of sender's account. nonce = number of transactions
nonce = w3.eth.getTransactionCount(checksum)
print("Nonce value:\n %i" % nonce)

# Ropsten chain ID is 3
chain_id = 3

# prepare the transaction, chainID 3 is ropsten
transaction_legacy = TransactionLegacy(nonce = nonce, gasPrice = 500000, gasLimit = 800000, to = binascii.unhexlify(receiver_addr.replace('0x', '')), value = 5, data = b'hello', v = chain_id, r = 0, s = 0)
transaction_1559 = RawTransactionType1559(chainId = chain_id, nonce = nonce, maxPriorityFeePerGas = 150000, maxFeePerGas = 150000, gas = 210000,
                                          to = binascii.unhexlify(receiver_addr.replace('0x', '')), value = 10, data = b'world', accessList = [])

#----------------------------------------Send Transaction Legacy with zymkey signature-----------------------------------------------------------------
# Sign a transaction with a signature generated by hsm6

# RLP encode the transaction
encoded_transaction = rlp.encode(transaction_legacy)
print("encoded transaction:\n%s" % binascii.hexlify(encoded_transaction).decode("utf-8"))

# Per Ethereum standards, Keccak hash rlp encoded transaction
keccak_hash=keccak.new(digest_bits=256)
keccak_hash.update(encoded_transaction)
print("keccak_hash:\n%s" % keccak_hash.hexdigest())

# sign the transaction hash and calculate v, r, s values
signature, y_parity = zymkey.client.sign_digest(keccak_hash, zymkey_pub_key_slot, return_recid=True)
print ("ECDSA Signature:\n%s" % signature)
print("ECDSA Sig Length:\n%s" % len(signature))
print("ECDSA Sig Y Parity:\n%s" % y_parity.value)

# Verify the ECDSA signature.
verify_sig = zymkey.client.verify_digest(keccak_hash, signature, True, zymkey_pub_key_slot, False)
print("Verify sig:\n%s" % verify_sig)

# Signature consists of a R, S, V
# R is the first half of the signature converted to int
# S is the second half of the signature converted to int
# From EIP 155, V = chainId * 2 + 35 + recovery_id of public key
# Per Ethereum yellow paper, if S * 2 >= N, then S becomes N - S and Y-parity is flipped

N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
r = int.from_bytes(signature[:32], "big")
s = int.from_bytes(signature[-32:], "big")

y_parity = bool(y_parity.value)
if((s*2) >= N):
   y_parity = not y_parity
   s = N - s

v = chain_id * 2 + 35 + int(y_parity)

print ("R:\n%s" % r)
print ("S:\n%s" % s)
print ("V:\n%s" % v)

# RLP encode the transaction along with the full signature
signed_transaction = TransactionLegacy(transaction_legacy.nonce, transaction_legacy.gasPrice, transaction_legacy.gasLimit, transaction_legacy.to, transaction_legacy.value, transaction_legacy.data, v, r, s)
encoded_signed_transaction = rlp.encode(signed_transaction)
print("encoded signed transaction:\n%s" % binascii.hexlify(encoded_signed_transaction).decode("utf-8"))

# send raw transaction
transaction_result_hash = w3.eth.sendRawTransaction(encoded_signed_transaction)
print("Transaction broadcast hash:\n%s" % binascii.hexlify(transaction_result_hash).decode("utf-8"))
#-----------------------------------------------------------------------------------------------------------------------------------------

#------------------------------Send Transaction Type EIP-1559 with zymkey signature-------------------------------------------------------
# Sign a transaction with a signature generated by hsm6

# RLP encode the transaction. Eip-1559 transaction is assigned type_id "2".
# So the payload is 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s])
encoded_transaction = bytes([2]) + rlp.encode(transaction_1559)
print("encoded transaction:\n%s" %  binascii.hexlify(encoded_transaction).decode("utf-8"))

# Per Ethereum standards, Keccak hash rlp encoded transaction
keccak_hash=keccak.new(digest_bits=256)
keccak_hash.update(encoded_transaction)
print("keccak_hash:\n%s" % keccak_hash.hexdigest())

# sign the transaction hash and calculate y-parity, r, s values
signature, y_parity = zymkey.client.sign_digest(keccak_hash, zymkey_pub_key_slot, return_recid=True)
print ("ECDSA Signature:\n%s" % signature)
print("ECDSA Sig Length:\n%s" % len(signature))
print("ECDSA Sig Y Parity:\n%s" % y_parity.value)

# Verify the ECDSA signature.
verify_sig = zymkey.client.verify_digest(keccak_hash, signature, True, zymkey_pub_key_slot, False)
print("Verify sig:\n%s" % verify_sig)

# Signature consists of a R, S, Y-parity
# R is the first half of the signature converted to int
# S is the second half of the signature converted to int
# Per Ethereum yellow paper, if S * 2 >= N, then S becomes N - S and Y-parity is flipped

N = 115792089237316195423570985008687907852837564279074904382605163141518161494337
r = int.from_bytes(signature[:32], "big")
s = int.from_bytes(signature[-32:], "big")

y_parity = bool(y_parity.value)
if((s*2) >= N):
   y_parity = not y_parity
   s = N - s

print ("R:\n%s" % r)
print ("S:\n%s" % s)
print ("y_parity:\n%s" % y_parity)

# RLP encode the transaction along with the full signature
signed_transaction = SignedTransactionType1559(transaction_1559.chainId, transaction_1559.nonce, transaction_1559.maxPriorityFeePerGas, transaction_1559.maxFeePerGas, transaction_1559.gas, transaction_1559.to, transaction_1559.value, transaction_1559.data, transaction_1559.accessList, y_parity, r, s)

# Tack on the same transaction type byte to the final transaction before being sent out
encoded_signed_transaction = bytes([2]) + rlp.encode(signed_transaction)
print("encoded signed transaction:\n%s" % binascii.hexlify(encoded_signed_transaction).decode("utf-8"))

# send raw transaction
transaction_result_hash = w3.eth.sendRawTransaction(encoded_signed_transaction)
print("Transaction broadcast hash:\n%s" % binascii.hexlify(transaction_result_hash).decode("utf-8"))
#---------------------------------------------------------------------------------------------------------------------------------