Class: BlockGiven::SignedTransaction

Inherits:
Object
  • Object
show all
Defined in:
lib/block_given/signed_transaction.rb

Overview

A signed transaction that has not been broadcast yet. Its hash is derived from the signed bytes, so it is known before any network call: persist hash and nonce, then broadcast. Whatever happens to the RPC call, the transaction can be found again with client.transaction(hash).

signed = registry.prepare_write(:record, movement_id, tx: { nonce: call.nonce })
call.update!(tx_hash: signed.hash, nonce: signed.nonce, status: :submitted)
signed.broadcast                                  # => BlockGiven::Transaction

signed.replacement.broadcast                      # same nonce, fees bumped by 12.5%

hash is the transaction hash (a String, like Transaction#hash), so instances are not usable as Hash keys; compare them with ==.

Constant Summary collapse

MIN_REPLACEMENT_MULTIPLIER =

Nodes reject a same-nonce replacement whose fees are not at least 10% higher.

1.1
DEFAULT_REPLACEMENT_MULTIPLIER =
1.125

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw:, params:, wallet:, interface: nil) ⇒ SignedTransaction

interface: an Abi::Interface used to name custom errors when the node rejects the broadcast with revert data (set by Contract#prepare_write).



61
62
63
64
65
66
67
# File 'lib/block_given/signed_transaction.rb', line 61

def initialize(raw:, params:, wallet:, interface: nil)
  @raw = Utils.prefix_hex(raw)
  @params = params.freeze
  @wallet = wallet
  @interface = interface
  @hash = Utils.keccak256(@raw)
end

Instance Attribute Details

#hashObject (readonly)

Returns the value of attribute hash.



57
58
59
# File 'lib/block_given/signed_transaction.rb', line 57

def hash
  @hash
end

#interfaceObject (readonly)

Returns the value of attribute interface.



57
58
59
# File 'lib/block_given/signed_transaction.rb', line 57

def interface
  @interface
end

#paramsObject (readonly)

Returns the value of attribute params.



57
58
59
# File 'lib/block_given/signed_transaction.rb', line 57

def params
  @params
end

#rawObject (readonly)

Returns the value of attribute raw.



57
58
59
# File 'lib/block_given/signed_transaction.rb', line 57

def raw
  @raw
end

#walletObject (readonly)

Returns the value of attribute wallet.



57
58
59
# File 'lib/block_given/signed_transaction.rb', line 57

def wallet
  @wallet
end

Class Method Details

.from_raw(raw, wallet:, interface: nil) ⇒ Object

Rebuilds a SignedTransaction from persisted raw bytes (after a restart, typically), so it can be broadcast again or replaced. The bytes must have been signed by wallet.



24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/block_given/signed_transaction.rb', line 24

def self.from_raw(raw, wallet:, interface: nil)
  decoded = begin
    Eth::Tx.decode(Utils.prefix_hex(raw))
  rescue StandardError => e
    raise InvalidArgumentError, "cannot decode signed transaction: #{e.message}"
  end
  sender = Utils.checksum_address(Utils.prefix_hex(decoded.sender)) # eth returns unprefixed hex
  unless Utils.same_address?(sender, wallet.address)
    raise InvalidArgumentError, "signed transaction was sent by #{sender}, not by wallet #{wallet.address}"
  end

  new(raw: raw, params: params_from(decoded, sender), wallet: wallet, interface: interface)
end

Instance Method Details

#==(other) ⇒ Object



119
# File 'lib/block_given/signed_transaction.rb', line 119

def ==(other) = other.is_a?(SignedTransaction) && other.raw == raw

#broadcastObject Also known as: submit

eth_sendRawTransaction. Returns a Transaction carrying the locally computed hash.



86
87
88
89
90
91
92
93
94
# File 'lib/block_given/signed_transaction.rb', line 86

def broadcast
  sent = with_decoded_errors { client.send_raw_transaction(raw) }
  unless sent.hash.to_s.casecmp?(hash)
    client.logger.warn do
      "[block_given] node returned #{sent.hash} for signed transaction #{hash}"
    end
  end
  transaction
end

#chain_idObject



79
# File 'lib/block_given/signed_transaction.rb', line 79

def chain_id = params[:chain_id]

#clientObject



71
# File 'lib/block_given/signed_transaction.rb', line 71

def client = wallet.client

#dataObject



76
# File 'lib/block_given/signed_transaction.rb', line 76

def data = params[:data]

#fromObject



73
# File 'lib/block_given/signed_transaction.rb', line 73

def from = params[:from]

#gasObject



78
# File 'lib/block_given/signed_transaction.rb', line 78

def gas = params[:gas]

#gas_priceObject



82
# File 'lib/block_given/signed_transaction.rb', line 82

def gas_price = params[:gas_price]

#inspectObject



120
# File 'lib/block_given/signed_transaction.rb', line 120

def inspect = "#<BlockGiven::SignedTransaction #{hash} nonce=#{nonce} to=#{to}>"

#legacy?Boolean

Returns:

  • (Boolean)


83
# File 'lib/block_given/signed_transaction.rb', line 83

def legacy? = !gas_price.nil?

#max_fee_per_gasObject



80
# File 'lib/block_given/signed_transaction.rb', line 80

def max_fee_per_gas = params[:max_fee_per_gas]

#max_priority_fee_per_gasObject



81
# File 'lib/block_given/signed_transaction.rb', line 81

def max_priority_fee_per_gas = params[:max_priority_fee_per_gas]

#nonceObject



77
# File 'lib/block_given/signed_transaction.rb', line 77

def nonce = params[:nonce]

#replacement(fee_multiplier: DEFAULT_REPLACEMENT_MULTIPLIER) ⇒ Object

Re-signs the same payload with the same nonce and higher fees, to replace a transaction stuck in the mempool. Fees are the max of (current fees x fee_multiplier) and a fresh estimate from the node, so the replacement also catches up with the market. Both transactions share a nonce: only one can be mined.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/block_given/signed_transaction.rb', line 104

def replacement(fee_multiplier: DEFAULT_REPLACEMENT_MULTIPLIER)
  if fee_multiplier < MIN_REPLACEMENT_MULTIPLIER
    raise InvalidArgumentError, "fee_multiplier must be >= #{MIN_REPLACEMENT_MULTIPLIER} (got #{fee_multiplier})"
  end
  raise InvalidArgumentError, "cannot replace a transaction without fee params (use .from_raw)" unless fees?

  wallet.signed_transaction(
    to: to, value: value, data: data, gas: gas, nonce: nonce, chain_id: chain_id,
    access_list: params[:access_list], **with_decoded_errors { bumped_fees(fee_multiplier) }
  ).with_interface(interface)
end

#toObject



74
# File 'lib/block_given/signed_transaction.rb', line 74

def to = params[:to]

#to_hObject



116
# File 'lib/block_given/signed_transaction.rb', line 116

def to_h = params.merge(hash: hash, raw: raw)

#to_sObject



118
# File 'lib/block_given/signed_transaction.rb', line 118

def to_s = hash

#transactionObject

The Transaction handle for this hash, without broadcasting (status, receipt...).



98
# File 'lib/block_given/signed_transaction.rb', line 98

def transaction = client.transaction(hash)

#valueObject



75
# File 'lib/block_given/signed_transaction.rb', line 75

def value = params[:value]

#with_interface(interface) ⇒ Object



69
# File 'lib/block_given/signed_transaction.rb', line 69

def with_interface(interface) = self.class.new(raw: raw, params: params, wallet: wallet, interface: interface)