# Copyright (c) 2016-2022 The Ruby-Eth Contributors## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.# Provides the {Eth} module.moduleEth# Provides the `Tx` module supporting various transaction types.moduleTx# Provides support for EIP-1559 transactions utilizing EIP-2718# types and envelopes.# Ref: https://eips.ethereum.org/EIPS/eip-1559classEip1559# The EIP-155 Chain ID.# Ref: https://eips.ethereum.org/EIPS/eip-155attr_reader:chain_id# The transaction nonce provided by the signer.attr_reader:signer_nonce# The transaction max priority fee per gas in Wei.attr_reader:max_priority_fee_per_gas# The transaction max fee per gas in Wei.attr_reader:max_fee_per_gas# The gas limit for the transaction.attr_reader:gas_limit# The recipient address.attr_reader:destination# The transaction amount in Wei.attr_reader:amount# The transaction data payload.attr_reader:payload# An optional EIP-2930 access list.# Ref: https://eips.ethereum.org/EIPS/eip-2930attr_reader:access_list# The signature's y-parity byte (not v).attr_reader:signature_y_parity# The signature `r` value.attr_reader:signature_r# The signature `s` value.attr_reader:signature_s# The sender address.attr_reader:sender# The transaction type.attr_reader:type# Create a type-2 (EIP-1559) transaction payload object that# can be prepared for envelope, signature and broadcast.# Ref: https://eips.ethereum.org/EIPS/eip-1559## @param params [Hash] all necessary transaction fields.# @option params [Integer] :chain_id the chain ID.# @option params [Integer] :nonce the signer nonce.# @option params [Integer] :priority_fee the max priority fee per gas.# @option params [Integer] :max_gas_fee the max transaction fee per gas.# @option params [Integer] :gas_limit the gas limit.# @option params [Eth::Address] :from the sender address.# @option params [Eth::Address] :to the reciever address.# @option params [Integer] :value the transaction value.# @option params [String] :data the transaction data payload.# @option params [Array] :access_list an optional access list.# @raise [ParameterError] if gas limit is too low.definitialize(params)fields={recovery_id: nil,r: 0,s: 0}.mergeparams# populate optional fields with serializable empty valuesfields[:chain_id]=Tx.sanitize_chainfields[:chain_id]fields[:from]=Tx.sanitize_addressfields[:from]fields[:to]=Tx.sanitize_addressfields[:to]fields[:value]=Tx.sanitize_amountfields[:value]fields[:data]=Tx.sanitize_datafields[:data]# ensure sane values for all mandatory fieldsfields=Tx.validate_paramsfieldsfields[:access_list]=Tx.sanitize_listfields[:access_list]# ensure gas limit is not too lowminimum_cost=Tx.estimate_intrinsic_gasfields[:data],fields[:access_list]raiseParameterError,"Transaction gas limit is too low, try #{minimum_cost}!"iffields[:gas_limit].to_i<minimum_cost# populate class attributes@signer_nonce=fields[:nonce].to_i@max_priority_fee_per_gas=fields[:priority_fee].to_i@max_fee_per_gas=fields[:max_gas_fee].to_i@gas_limit=fields[:gas_limit].to_i@sender=fields[:from].to_s@destination=fields[:to].to_s@amount=fields[:value].to_i@payload=fields[:data]@access_list=fields[:access_list]# the signature v is set to the chain id for unsigned transactions@signature_y_parity=fields[:recovery_id]@chain_id=fields[:chain_id]# the signature fields are empty for unsigned transactions.@signature_r=fields[:r]@signature_s=fields[:s]# last but not least, set the type.@type=TYPE_1559end# Overloads the constructor for decoding raw transactions and creating unsigned copies.konstructor:decode,:unsigned_copy# Decodes a raw transaction hex into an {Eth::Tx::Eip1559}# transaction object.## @param hex [String] the raw transaction hex-string.# @return [Eth::Tx::Eip1559] transaction payload.# @raise [TransactionTypeError] if transaction type is invalid.# @raise [ParameterError] if transaction is missing fields.# @raise [DecoderError] if transaction decoding fails.defdecode(hex)hex=Util.remove_hex_prefixhextype=hex[0,2]raiseTransactionTypeError,"Invalid transaction type #{type}!"iftype.to_i(16)!=TYPE_1559bin=Util.hex_to_binhex[2..]tx=Rlp.decodebin# decoded transactions always have 9 + 3 fields, even if they are empty or zeroraiseParameterError,"Transaction missing fields!"iftx.size<9# populate the 9 payload fieldschain_id=Util.deserialize_big_endian_to_inttx[0]nonce=Util.deserialize_big_endian_to_inttx[1]priority_fee=Util.deserialize_big_endian_to_inttx[2]max_gas_fee=Util.deserialize_big_endian_to_inttx[3]gas_limit=Util.deserialize_big_endian_to_inttx[4]to=Util.bin_to_hextx[5]value=Util.deserialize_big_endian_to_inttx[6]data=tx[7]access_list=tx[8]# populate class attributes@chain_id=chain_id.to_i@signer_nonce=nonce.to_i@max_priority_fee_per_gas=priority_fee.to_i@max_fee_per_gas=max_gas_fee.to_i@gas_limit=gas_limit.to_i@destination=to.to_s@amount=value.to_i@payload=data@access_list=access_list# populate the 3 signature fieldsiftx.size==9_set_signature(nil,0,0)elsiftx.size==12recovery_id=Util.bin_to_hex(tx[9]).to_i(16)r=Util.bin_to_hextx[10]s=Util.bin_to_hextx[11]# allows us to force-setting a signature if the transaction is signed already_set_signature(recovery_id,r,s)elseraise_errorDecoderError,"Cannot decode EIP-1559 payload!"end# last but not least, set the type.@type=TYPE_1559# recover sender addressv=Chain.to_vrecovery_id,chain_idpublic_key=Signature.recover(unsigned_hash,"#{r}#{s}#{v.to_s(16)}",chain_id)address=Util.public_key_to_address(public_key).to_s@sender=Tx.sanitize_addressaddressend# Creates an unsigned copy of a transaction payload.## @param tx [Eth::Tx::Eip1559] an EIP-1559 transaction payload.# @return [Eth::Tx::Eip1559] an unsigned EIP-1559 transaction payload.# @raise [TransactionTypeError] if transaction type does not match.defunsigned_copy(tx)# not checking transaction validity unless it's of a different classraiseTransactionTypeError,"Cannot copy transaction of different payload type!"unlesstx.instance_of?Tx::Eip1559# populate class attributes@signer_nonce=tx.signer_nonce@max_priority_fee_per_gas=tx.max_priority_fee_per_gas@max_fee_per_gas=tx.max_fee_per_gas@gas_limit=tx.gas_limit@destination=tx.destination@amount=tx.amount@payload=tx.payload@access_list=tx.access_list@chain_id=tx.chain_id# force-set signature to unsigned_set_signature(nil,0,0)# keep the 'from' field blank@sender=Tx.sanitize_addressnil# last but not least, set the type.@type=TYPE_1559end# Sign the transaction with a given key.## @param key [Eth::Key] the key-pair to use for signing.# @return [String] a transaction hash.# @raise [Signature::SignatureError] if transaction is already signed.# @raise [Signature::SignatureError] if sender address does not match signing key.defsign(key)ifTx.is_signed?selfraiseSignature::SignatureError,"Transaction is already signed!"end# ensure the sender address matches the given keyunless@sender.nil?orsender.empty?signer_address=Tx.sanitize_addresskey.address.to_sfrom_address=Tx.sanitize_address@senderraiseSignature::SignatureError,"Signer does not match sender"unlesssigner_address==from_addressend# sign a keccak hash of the unsigned, encoded transactionsignature=key.sign(unsigned_hash,@chain_id)r,s,v=Signature.dissectsignaturerecovery_id=Chain.to_recovery_idv.to_i(16),@chain_id@signature_y_parity=recovery_id@signature_r=r@signature_s=sreturnhashend# Encodes a raw transaction object, wraps it in an EIP-2718 envelope# with an EIP-1559 type prefix.## @return [String] a raw, RLP-encoded EIP-1559 type transaction object.# @raise [Signature::SignatureError] if the transaction is not yet signed.defencodedunlessTx.is_signed?selfraiseSignature::SignatureError,"Transaction is not signed!"endtx_data=[]tx_data.pushUtil.serialize_int_to_big_endian@chain_idtx_data.pushUtil.serialize_int_to_big_endian@signer_noncetx_data.pushUtil.serialize_int_to_big_endian@max_priority_fee_per_gastx_data.pushUtil.serialize_int_to_big_endian@max_fee_per_gastx_data.pushUtil.serialize_int_to_big_endian@gas_limittx_data.pushUtil.hex_to_bin@destinationtx_data.pushUtil.serialize_int_to_big_endian@amounttx_data.pushRlp::Sedes.binary.serialize@payloadtx_data.pushRlp::Sedes.infer(@access_list).serialize@access_listtx_data.pushUtil.serialize_int_to_big_endian@signature_y_paritytx_data.pushUtil.serialize_int_to_big_endian@signature_rtx_data.pushUtil.serialize_int_to_big_endian@signature_stx_encoded=Rlp.encodetx_data# create an EIP-2718 envelope with EIP-1559 type payloadtx_type=Util.serialize_int_to_big_endian@typereturn"#{tx_type}#{tx_encoded}"end# Gets the encoded, enveloped, raw transaction hex.## @return [String] the raw transaction hex.defhexUtil.bin_to_hexencodedend# Gets the transaction hash.## @return [String] the transaction hash.defhashUtil.bin_to_hexUtil.keccak256encodedend# Encodes the unsigned transaction payload in an EIP-1559 envelope,# required for signing.## @return [String] an RLP-encoded, unsigned, enveloped EIP-1559 transaction.defunsigned_encodedtx_data=[]tx_data.pushUtil.serialize_int_to_big_endian@chain_idtx_data.pushUtil.serialize_int_to_big_endian@signer_noncetx_data.pushUtil.serialize_int_to_big_endian@max_priority_fee_per_gastx_data.pushUtil.serialize_int_to_big_endian@max_fee_per_gastx_data.pushUtil.serialize_int_to_big_endian@gas_limittx_data.pushUtil.hex_to_bin@destinationtx_data.pushUtil.serialize_int_to_big_endian@amounttx_data.pushRlp::Sedes.binary.serialize@payloadtx_data.pushRlp::Sedes.infer(@access_list).serialize@access_listtx_encoded=Rlp.encodetx_data# create an EIP-2718 envelope with EIP-1559 type payload (unsigned)tx_type=Util.serialize_int_to_big_endian@typereturn"#{tx_type}#{tx_encoded}"end# Gets the sign-hash required to sign a raw transaction.## @return [String] a Keccak-256 hash of an unsigned transaction.defunsigned_hashUtil.keccak256unsigned_encodedendprivate# Force-sets an existing signature of a decoded transaction.def_set_signature(recovery_id,r,s)@signature_y_parity=recovery_id@signature_r=r@signature_s=sendendendend