# 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 legacy support for transactions on blockchains that do not# implement EIP-1559 but still want to utilize EIP-2718 envelopes.# Ref: https://eips.ethereum.org/EIPS/eip-2930classEip2930# 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 gas price for the transaction in Wei.attr_reader:gas_price# 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 legacy type-1 (EIP-2930) transaction payload object that# can be prepared for envelope, signature and broadcast. Should not# be used unless there is no EIP-1559 support.# Ref: https://eips.ethereum.org/EIPS/eip-2930### @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] :gas_price the gas price.# @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=Tx.validate_legacy_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@gas_price=fields[:gas_price].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_2930end# Overloads the constructor for decoding raw transactions and creating unsigned copies.konstructor:decode,:unsigned_copy# Decodes a raw transaction hex into an {Eth::Tx::Eip2930}# transaction object.## @param hex [String] the raw transaction hex-string.# @return [Eth::Tx::Eip2930] 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_2930bin=Util.hex_to_binhex[2..]tx=Rlp.decodebin# decoded transactions always have 8 + 3 fields, even if they are empty or zeroraiseParameterError,"Transaction missing fields!"iftx.size<8# populate the 8 payload fieldschain_id=Util.deserialize_big_endian_to_inttx[0]nonce=Util.deserialize_big_endian_to_inttx[1]gas_price=Util.deserialize_big_endian_to_inttx[2]gas_limit=Util.deserialize_big_endian_to_inttx[3]to=Util.bin_to_hextx[4]value=Util.deserialize_big_endian_to_inttx[5]data=tx[6]access_list=tx[7]# populate class attributes@chain_id=chain_id.to_i@signer_nonce=nonce.to_i@gas_price=gas_price.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==8_set_signature(nil,0,0)elsiftx.size==11recovery_id=Util.bin_to_hex(tx[8]).to_i(16)r=Util.bin_to_hextx[9]s=Util.bin_to_hextx[10]# allows us to force-setting a signature if the transaction is signed already_set_signature(recovery_id,r,s)elseraise_errorDecoderError,"Cannot decode EIP-2930 payload!"end# last but not least, set the type.@type=TYPE_2930# 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::Eip2930] an EIP-2930 transaction payload.# @return [Eth::Tx::Eip2930] an unsigned EIP-2930 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::Eip2930# populate class attributes@signer_nonce=tx.signer_nonce@gas_price=tx.gas_price@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_2930end# 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-2930 type prefix.## @return [String] a raw, RLP-encoded EIP-2930 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@gas_pricetx_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-2930 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-2930 envelope,# required for signing.## @return [String] an RLP-encoded, unsigned, enveloped EIP-2930 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@gas_pricetx_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-2930 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