block_given 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/README.md +92 -55
  4. data/lib/block_given/abi/codec/decoder.rb +111 -0
  5. data/lib/block_given/abi/codec.rb +170 -0
  6. data/lib/block_given/abi/coder.rb +122 -11
  7. data/lib/block_given/abi/custom_error.rb +33 -1
  8. data/lib/block_given/abi/event.rb +85 -3
  9. data/lib/block_given/abi/function.rb +94 -2
  10. data/lib/block_given/abi/interface.rb +99 -4
  11. data/lib/block_given/abi/parameter.rb +60 -3
  12. data/lib/block_given/abi/standards/erc1155.rb +41 -0
  13. data/lib/block_given/abi/standards/erc20.rb +33 -0
  14. data/lib/block_given/abi/standards/erc4626.rb +41 -0
  15. data/lib/block_given/abi/standards/erc721.rb +47 -0
  16. data/lib/block_given/abi/standards.rb +124 -0
  17. data/lib/block_given/abi/type.rb +138 -0
  18. data/lib/block_given/chain.rb +114 -2
  19. data/lib/block_given/client.rb +322 -20
  20. data/lib/block_given/configuration.rb +85 -3
  21. data/lib/block_given/connectors/alchemy.rb +38 -4
  22. data/lib/block_given/connectors/base.rb +32 -5
  23. data/lib/block_given/connectors/http.rb +91 -5
  24. data/lib/block_given/connectors/stub.rb +70 -4
  25. data/lib/block_given/contract.rb +422 -26
  26. data/lib/block_given/crypto/keccak.rb +152 -0
  27. data/lib/block_given/crypto/secp256k1.rb +168 -0
  28. data/lib/block_given/crypto.rb +22 -0
  29. data/lib/block_given/eip712.rb +199 -0
  30. data/lib/block_given/errors.rb +134 -14
  31. data/lib/block_given/event.rb +51 -1
  32. data/lib/block_given/normalizer.rb +27 -2
  33. data/lib/block_given/poller.rb +177 -15
  34. data/lib/block_given/receipt.rb +63 -3
  35. data/lib/block_given/rlp.rb +146 -0
  36. data/lib/block_given/signed_transaction.rb +148 -47
  37. data/lib/block_given/transaction.rb +103 -12
  38. data/lib/block_given/transaction_envelope/fields.rb +104 -0
  39. data/lib/block_given/transaction_envelope.rb +183 -0
  40. data/lib/block_given/utils.rb +143 -9
  41. data/lib/block_given/version.rb +2 -1
  42. data/lib/block_given/wallet.rb +216 -35
  43. data/lib/block_given.rb +67 -4
  44. metadata +19 -23
@@ -1,66 +1,240 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- # A private key + (optional) client, able to sign and send transactions
5
- # (viem's WalletClient + Account).
4
+ # A private key plus an optional {Client}, able to sign messages and to prepare, sign and send
5
+ # transactions (the equivalent of viem's WalletClient + Account).
6
6
  #
7
+ # Every value and fee is an Integer amount of wei. Transactions are EIP-1559 (type 2) by default;
8
+ # passing `gas_price:` to any transaction method builds a legacy (type 0) transaction instead.
9
+ # Missing fields (nonce, gas, fees, chain id) are resolved from {#client} at signing time.
10
+ #
11
+ # @example Create a wallet and send ether
7
12
  # wallet = BlockGiven::Wallet.new(private_key: ENV["PRIVATE_KEY"])
8
- # wallet.address
13
+ # wallet.address # => "0xAbC..." (EIP-55 checksummed)
9
14
  # wallet.send_transaction(to: "0x...", value: BlockGiven::Utils.parse_ether("0.01")).wait
10
15
  class Wallet
16
+ # @return [String] the EIP-55 checksummed address derived from the private key
11
17
  attr_reader :address
12
18
 
19
+ # Creates a wallet with a freshly generated random private key.
20
+ #
21
+ # @param client [Client, nil] client used for RPC calls; when nil, {BlockGiven.client} is used
22
+ # @return [Wallet] a new wallet holding the generated key
13
23
  def self.generate(client: nil)
14
- new(private_key: Eth::Key.new.private_hex, client: client)
24
+ new(private_key: Crypto::Secp256k1.int_to_bytes(Crypto::Secp256k1.generate_private_key).unpack1("H*"),
25
+ client: client)
15
26
  end
16
27
 
28
+ # Builds a wallet from an existing private key.
29
+ #
30
+ # @param private_key [String] 32-byte private key as 64 hex characters, with or without the `0x` prefix
31
+ # (surrounding whitespace is ignored)
32
+ # @param client [Client, nil] client used for nonce, gas and fee lookups and for broadcasting; when nil,
33
+ # every call falls back to {BlockGiven.client}
34
+ # @raise [BlockGiven::InvalidArgumentError] when the key is not exactly 32 bytes of hex, or is not a valid
35
+ # secp256k1 key (zero, or not below the curve order)
36
+ # @example
37
+ # wallet = BlockGiven::Wallet.new(private_key: "0x4c0883a6...d1e6") # 64 hex chars
38
+ # wallet.address # => "0x..."
17
39
  def initialize(private_key:, client: nil)
18
40
  hex = Utils.strip_hex(private_key.to_s.strip)
19
41
  raise InvalidArgumentError, "private key must be 32 bytes hex" unless hex.match?(/\A[0-9a-fA-F]{64}\z/)
20
42
 
21
- @key = Eth::Key.new(priv: hex)
22
- @address = @key.address.checksummed
43
+ @private_key = hex.to_i(16)
44
+ unless Crypto::Secp256k1.valid_private_key?(@private_key)
45
+ raise InvalidArgumentError, "private key is out of the secp256k1 range"
46
+ end
47
+
48
+ @public_key = Crypto::Secp256k1.public_key(@private_key)
49
+ @address = Crypto.address(@public_key)
23
50
  @client = client
24
51
  end
25
52
 
26
- def private_key = Utils.prefix_hex(@key.private_hex)
27
- def public_key = Utils.prefix_hex(@key.public_hex)
53
+ # The private key, for persistence or export. It is never included in {#inspect} or {#to_s}.
54
+ #
55
+ # @return [String] the 32-byte private key as `0x`-prefixed hex
56
+ def private_key = Utils.bin_to_hex(Crypto::Secp256k1.int_to_bytes(@private_key))
28
57
 
58
+ # The uncompressed public key matching {#private_key}.
59
+ #
60
+ # @return [String] the 65-byte public key as `0x`-prefixed hex
61
+ def public_key = Utils.bin_to_hex(@public_key)
62
+
63
+ # The client used for every RPC call made by this wallet.
64
+ #
65
+ # @return [Client] the client given to {#initialize}, or {BlockGiven.client} when none was given
29
66
  def client = @client || BlockGiven.client
67
+
68
+ # Returns a copy of this wallet (same key) bound to another client, e.g. to sign on another chain.
69
+ #
70
+ # @param client [Client] the client the copy will use
71
+ # @return [Wallet] a new wallet with the same private key and the given client
30
72
  def with_client(client) = self.class.new(private_key: private_key, client: client)
73
+
74
+ # The chain of {#client}.
75
+ #
76
+ # @return [Chain] the chain the wallet's transactions are built for
31
77
  def chain = client.chain
32
78
 
79
+ # Balance of {#address} in wei.
80
+ #
81
+ # @param block [Symbol, Integer, String] block tag (`:latest`, `:pending`, `:safe`, `:finalized`, `:earliest`)
82
+ # or block number
83
+ # @return [Integer] the balance in wei at that block
33
84
  def balance(block: :latest) = client.get_balance(address, block: block)
85
+
86
+ # Transaction count of {#address}, i.e. the nonce the next transaction should use.
87
+ #
88
+ # @param block [Symbol, Integer, String] block tag or number; defaults to `:pending` so that transactions
89
+ # still waiting in the mempool are counted
90
+ # @return [Integer] the transaction count at that block
34
91
  def nonce(block: :pending) = client.get_transaction_count(address, block: block)
35
92
 
36
- # EIP-191 personal_sign. Returns the 65-byte signature as hex.
93
+ # Signs a message with the EIP-191 `personal_sign` scheme.
94
+ #
95
+ # The message is prefixed with `"\x19Ethereum Signed Message:\n" + length` before hashing, so the signature
96
+ # can be verified with `ecrecover` or viem's `verifyMessage`.
97
+ #
98
+ # @param message [String] the message to sign, as a plain string
99
+ # @return [String] the 65-byte signature (r, s, v) as `0x`-prefixed hex
100
+ # @example
101
+ # signature = wallet.sign_message("Login to Bolero at 2024-01-01")
102
+ # signature # => "0x..." (132 hex characters)
37
103
  def sign_message(message)
38
- Utils.prefix_hex(@key.personal_sign(message))
104
+ bytes = message.to_s.b
105
+ sign_digest(Crypto::Keccak.digest("\x19Ethereum Signed Message:\n#{bytes.bytesize}".b + bytes))
39
106
  end
40
107
 
41
- # EIP-712 typed data (Hash with :types, :primaryType, :domain, :message).
108
+ # Signs EIP-712 typed structured data.
109
+ #
110
+ # @param typed_data [Hash] the EIP-712 payload with `:types`, `:primaryType`, `:domain` and `:message` keys
111
+ # (String keys are accepted too). `EIP712Domain` may be omitted from `:types`: it is then derived from the
112
+ # domain keys present. Arrays are hashed as the specification (and viem) define them
113
+ # @return [String] the 65-byte signature (r, s, v) as `0x`-prefixed hex
114
+ # @raise [BlockGiven::InvalidArgumentError] when a section is missing, a type is unknown or a value does not
115
+ # fit its type
116
+ # @example Sign an ERC-2612 permit
117
+ # wallet.sign_typed_data(
118
+ # types: { EIP712Domain: [...], Permit: [{ name: "owner", type: "address" }, ...] },
119
+ # primaryType: "Permit",
120
+ # domain: { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: "0x..." },
121
+ # message: { owner: wallet.address, spender: "0x...", value: 1_000_000, nonce: 0, deadline: 1_700_000_000 }
122
+ # )
42
123
  def sign_typed_data(typed_data)
43
- Utils.prefix_hex(@key.sign_typed_data(typed_data))
124
+ sign_digest(Eip712.hash(typed_data))
44
125
  end
45
126
 
46
- # Fills in nonce / gas / fees from the client and signs, without broadcasting.
47
- # Returns a BlockGiven::SignedTransaction: its #hash and #nonce are known before
48
- # any network call (persist them, then #broadcast). Same keywords as prepare_transaction;
49
- # pass gas_price: to build a legacy (type 0) transaction instead of EIP-1559.
127
+ # Resolves the missing fields, signs, and returns the transaction without broadcasting it.
128
+ #
129
+ # The returned {SignedTransaction} knows its {SignedTransaction#hash} (keccak of the signed bytes) and
130
+ # {SignedTransaction#nonce} before any network call: persist them, then call {SignedTransaction#broadcast}.
131
+ # See {#prepare_transaction} for how each missing field is resolved.
132
+ #
133
+ # @overload signed_transaction(to: nil, value: 0, data: nil, gas: nil, nonce: nil, max_fee_per_gas: nil,
134
+ # max_priority_fee_per_gas: nil, gas_price: nil, chain_id: nil, access_list: nil)
135
+ # @param to [String, nil] recipient address (any case, checksummed before signing); nil for contract creation
136
+ # @param value [Integer, String, Float, BigDecimal, Rational] amount of wei to send; Strings may be decimal
137
+ # or `0x` hex, non-Integer numerics must have no fractional part. Defaults to 0
138
+ # @param data [String, nil] calldata as hex (with or without `0x`); nil or empty means no calldata
139
+ # @param gas [Integer, nil] gas limit; defaults to `eth_estimateGas` multiplied by
140
+ # `BlockGiven.config.gas_multiplier` (rounded up)
141
+ # @param nonce [Integer, nil] nonce to use; defaults to the wallet's pending transaction count ({#nonce})
142
+ # @param max_fee_per_gas [Integer, nil] EIP-1559 max fee per gas in wei; defaults to
143
+ # {Client#estimate_fees_per_gas} when omitted (ignored when `gas_price:` is given)
144
+ # @param max_priority_fee_per_gas [Integer, nil] EIP-1559 priority fee per gas in wei; defaults to
145
+ # {Client#estimate_fees_per_gas} when omitted (ignored when `gas_price:` is given)
146
+ # @param gas_price [Integer, nil] gas price in wei; when given, a legacy (type 0) transaction is built and
147
+ # the EIP-1559 fee fields are not used. Never estimated: legacy transactions need an explicit value
148
+ # @param chain_id [Integer, nil] chain id for EIP-155 replay protection; defaults to `client.chain.id`
149
+ # @param access_list [Array<Hash>, nil] EIP-2930 access list; nil means an empty list (EIP-1559 only)
150
+ # @return [SignedTransaction] the signed transaction, ready to be broadcast or persisted
151
+ # @raise [BlockGiven::InvalidArgumentError] when `value:` is malformed or fractional, or when `to:` is nil
152
+ # without an explicit `gas:` (contract creation cannot be estimated here)
153
+ # @example Persist the hash before broadcasting, replace the transaction if it gets stuck
154
+ # signed = wallet.signed_transaction(to: usdc_address, data: calldata)
155
+ # record.update!(tx_hash: signed.hash, nonce: signed.nonce, raw_tx: signed.raw)
156
+ # tx = signed.broadcast # => BlockGiven::Transaction with the same hash
157
+ # signed.replacement.broadcast if tx.pending? # same nonce, fees bumped by 12.5%
50
158
  def signed_transaction(**params)
51
159
  prepared = prepare_transaction(**params)
52
- tx = Eth::Tx.new(to_eth_tx_params(prepared))
53
- tx.sign(@key)
54
- SignedTransaction.new(raw: Utils.prefix_hex(tx.hex), params: prepared, wallet: self)
160
+ SignedTransaction.new(raw: TransactionEnvelope.sign(prepared, @private_key), params: prepared, wallet: self)
55
161
  end
56
162
 
57
- # Signs and returns the raw tx hex.
163
+ # Signs a transaction and returns only the raw signed bytes, without broadcasting.
164
+ #
165
+ # Shorthand for `signed_transaction(**params).raw`; see {#signed_transaction} for the keyword semantics.
166
+ #
167
+ # @overload sign_transaction(to: nil, value: 0, data: nil, gas: nil, nonce: nil, max_fee_per_gas: nil,
168
+ # max_priority_fee_per_gas: nil, gas_price: nil, chain_id: nil, access_list: nil)
169
+ # @param to [String, nil] recipient address; nil for contract creation (then `gas:` is required)
170
+ # @param value [Integer, String, Float, BigDecimal, Rational] amount of wei to send (default 0)
171
+ # @param data [String, nil] calldata as hex
172
+ # @param gas [Integer, nil] gas limit; default `eth_estimateGas` times `BlockGiven.config.gas_multiplier`
173
+ # @param nonce [Integer, nil] nonce; default the wallet's pending transaction count
174
+ # @param max_fee_per_gas [Integer, nil] EIP-1559 max fee in wei; default from {Client#estimate_fees_per_gas}
175
+ # @param max_priority_fee_per_gas [Integer, nil] EIP-1559 priority fee in wei; default from
176
+ # {Client#estimate_fees_per_gas}
177
+ # @param gas_price [Integer, nil] gas price in wei; switches to a legacy (type 0) transaction
178
+ # @param chain_id [Integer, nil] chain id; default `client.chain.id`
179
+ # @param access_list [Array<Hash>, nil] EIP-2930 access list (EIP-1559 only)
180
+ # @return [String] the RLP-encoded signed transaction as `0x`-prefixed hex, as accepted by
181
+ # `eth_sendRawTransaction`
182
+ # @raise [BlockGiven::InvalidArgumentError] same conditions as {#signed_transaction}
58
183
  def sign_transaction(**params) = signed_transaction(**params).raw
59
184
 
60
- # Signs and broadcasts. Returns a BlockGiven::Transaction.
185
+ # Signs a transaction and broadcasts it right away.
186
+ #
187
+ # Shorthand for `signed_transaction(**params).broadcast`; see {#signed_transaction} for the keyword semantics.
188
+ #
189
+ # @overload send_transaction(to: nil, value: 0, data: nil, gas: nil, nonce: nil, max_fee_per_gas: nil,
190
+ # max_priority_fee_per_gas: nil, gas_price: nil, chain_id: nil, access_list: nil)
191
+ # @param to [String, nil] recipient address; nil for contract creation (then `gas:` is required)
192
+ # @param value [Integer, String, Float, BigDecimal, Rational] amount of wei to send (default 0)
193
+ # @param data [String, nil] calldata as hex
194
+ # @param gas [Integer, nil] gas limit; default `eth_estimateGas` times `BlockGiven.config.gas_multiplier`
195
+ # @param nonce [Integer, nil] nonce; default the wallet's pending transaction count
196
+ # @param max_fee_per_gas [Integer, nil] EIP-1559 max fee in wei; default from {Client#estimate_fees_per_gas}
197
+ # @param max_priority_fee_per_gas [Integer, nil] EIP-1559 priority fee in wei; default from
198
+ # {Client#estimate_fees_per_gas}
199
+ # @param gas_price [Integer, nil] gas price in wei; switches to a legacy (type 0) transaction
200
+ # @param chain_id [Integer, nil] chain id; default `client.chain.id`
201
+ # @param access_list [Array<Hash>, nil] EIP-2930 access list (EIP-1559 only)
202
+ # @return [Transaction] a handle on the broadcast transaction, carrying the locally computed hash
203
+ # @raise [BlockGiven::InvalidArgumentError] same conditions as {#signed_transaction}
204
+ # @raise [BlockGiven::RpcError] when the node rejects the transaction (a revert during the node's
205
+ # pre-check is raised as {ContractRevertError})
206
+ # @example
207
+ # tx = wallet.send_transaction(to: "0x...", value: BlockGiven::Utils.parse_ether("0.5"))
208
+ # tx.hash # => "0x..."
209
+ # receipt = tx.wait!
61
210
  def send_transaction(**params) = signed_transaction(**params).broadcast
62
211
 
63
- # Resolves every missing field (nonce, gas, fees, chain id) without signing.
212
+ # Resolves every missing transaction field from the client, without signing anything.
213
+ #
214
+ # This is the shared first step of {#signed_transaction}, {#sign_transaction} and {#send_transaction}.
215
+ # It normalises the inputs (checksummed `to`, Integer `value`, `0x`-prefixed `data`) and fills in the
216
+ # chain id, nonce, gas limit and fees from the node. When `gas_price:` is given the result describes a
217
+ # legacy (type 0) transaction and contains `:gas_price`; otherwise it contains `:max_fee_per_gas` and
218
+ # `:max_priority_fee_per_gas`.
219
+ #
220
+ # @param to [String, nil] recipient address (any case, checksummed in the result); nil for contract creation
221
+ # @param value [Integer, String, Float, BigDecimal, Rational, nil] amount of wei; Strings may be decimal or
222
+ # `0x` hex, non-Integer numerics must have no fractional part; nil is treated as 0
223
+ # @param data [String, nil] calldata as hex (with or without `0x`); nil or empty becomes `""`
224
+ # @param gas [Integer, nil] gas limit; defaults to `eth_estimateGas` (from {#address}, with `to`, `data` and
225
+ # `value`) multiplied by `BlockGiven.config.gas_multiplier` and rounded up
226
+ # @param nonce [Integer, nil] nonce; defaults to the wallet's pending transaction count ({#nonce})
227
+ # @param max_fee_per_gas [Integer, nil] EIP-1559 max fee per gas in wei; when either EIP-1559 field is
228
+ # omitted the missing one comes from {Client#estimate_fees_per_gas}. Ignored with `gas_price:`
229
+ # @param max_priority_fee_per_gas [Integer, nil] EIP-1559 priority fee per gas in wei; see `max_fee_per_gas`
230
+ # @param gas_price [Integer, nil] gas price in wei for a legacy (type 0) transaction; never estimated
231
+ # @param chain_id [Integer, nil] chain id; defaults to `client.chain.id`
232
+ # @param access_list [Array<Hash>, nil] EIP-2930 access list, kept as given (nil when omitted)
233
+ # @return [Hash{Symbol => Object}] the resolved parameters: `:from`, `:to`, `:value`, `:data`, `:chain_id`,
234
+ # `:nonce`, `:gas`, `:access_list`, plus either `:gas_price` or `:max_fee_per_gas` and
235
+ # `:max_priority_fee_per_gas`
236
+ # @raise [BlockGiven::InvalidArgumentError] when `value` cannot be coerced to an Integer amount of wei (for
237
+ # example a fractional Float), or when `to` is nil without an explicit `gas`
64
238
  def prepare_transaction(to: nil, value: 0, data: nil, gas: nil, nonce: nil, max_fee_per_gas: nil,
65
239
  max_priority_fee_per_gas: nil, gas_price: nil, chain_id: nil, access_list: nil)
66
240
  to = Utils.checksum_address(to) if to
@@ -90,14 +264,34 @@ module BlockGiven
90
264
  params
91
265
  end
92
266
 
267
+ # Two wallets are equal when they control the same address, whatever their client.
268
+ #
269
+ # @param other [Object] the object to compare with
270
+ # @return [Boolean] true when `other` is a {Wallet} with the same {#address}
93
271
  def ==(other) = other.is_a?(Wallet) && other.address == address
94
272
  alias eql? ==
273
+
274
+ # Hash code consistent with {#==}, so wallets can be used as Hash keys and in Sets.
275
+ #
276
+ # @return [Integer] the hash of {#address}
95
277
  def hash = address.hash
278
+
279
+ # @return [String] the checksummed {#address}
96
280
  def to_s = address
281
+
282
+ # Debug representation showing only the address. The private key is never printed.
283
+ #
284
+ # @return [String] e.g. `#<BlockGiven::Wallet 0xAbC...>`
97
285
  def inspect = "#<BlockGiven::Wallet #{address}>"
98
286
 
99
287
  private
100
288
 
289
+ # r || s || v with v = 27 + recovery id, as `personal_sign` and `eth_signTypedData` return.
290
+ def sign_digest(digest)
291
+ r, s, recovery_id = Crypto::Secp256k1.sign(digest, @private_key)
292
+ Utils.bin_to_hex(Crypto::Secp256k1.int_to_bytes(r) + Crypto::Secp256k1.int_to_bytes(s) + (27 + recovery_id).chr)
293
+ end
294
+
101
295
  def estimate_gas(to:, data:, value:)
102
296
  raise InvalidArgumentError, "contract creation requires an explicit gas: value" if to.nil?
103
297
 
@@ -120,18 +314,5 @@ module BlockGiven
120
314
  else raise InvalidArgumentError, "invalid value: #{value.inspect}"
121
315
  end
122
316
  end
123
-
124
- def to_eth_tx_params(params)
125
- base = {
126
- chain_id: params[:chain_id], nonce: params[:nonce], gas_limit: params[:gas],
127
- to: params[:to], value: params[:value], data: params[:data]
128
- }
129
- if params[:gas_price]
130
- base.merge(gas_price: params[:gas_price])
131
- else
132
- base[:access_list] = params[:access_list] || []
133
- base.merge(priority_fee: params[:max_priority_fee_per_gas], max_gas_fee: params[:max_fee_per_gas])
134
- end
135
- end
136
317
  end
137
318
  end
data/lib/block_given.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "eth"
4
-
5
3
  require_relative "block_given/version"
6
4
  require_relative "block_given/errors"
7
5
  require_relative "block_given/utils"
6
+ require_relative "block_given/crypto"
7
+ require_relative "block_given/rlp"
8
8
  require_relative "block_given/chain"
9
9
  require_relative "block_given/configuration"
10
10
  require_relative "block_given/connectors/base"
@@ -15,31 +15,74 @@ require_relative "block_given/normalizer"
15
15
  require_relative "block_given/poller"
16
16
  require_relative "block_given/receipt"
17
17
  require_relative "block_given/transaction"
18
+ require_relative "block_given/transaction_envelope"
19
+ require_relative "block_given/transaction_envelope/fields"
18
20
  require_relative "block_given/signed_transaction"
19
21
  require_relative "block_given/client"
22
+ require_relative "block_given/eip712"
20
23
  require_relative "block_given/wallet"
24
+ require_relative "block_given/abi/type"
25
+ require_relative "block_given/abi/codec"
26
+ require_relative "block_given/abi/codec/decoder"
21
27
  require_relative "block_given/abi/parameter"
22
28
  require_relative "block_given/abi/coder"
23
29
  require_relative "block_given/abi/function"
24
30
  require_relative "block_given/abi/event"
25
31
  require_relative "block_given/abi/custom_error"
26
32
  require_relative "block_given/abi/interface"
33
+ require_relative "block_given/abi/standards"
27
34
  require_relative "block_given/event"
28
35
  require_relative "block_given/contract"
29
36
  require_relative "block_given/railtie" if defined?(Rails::Railtie)
30
37
 
31
38
  # viem-inspired toolkit to read from and write to EVM smart contracts.
32
39
  #
40
+ # The gem is organised around a handful of entry points:
41
+ #
42
+ # - {BlockGiven.configure} sets the global {Configuration}: JSON-RPC connector, chain, polling and fee
43
+ # defaults, and the directory application ABIs live in.
44
+ # - {BlockGiven.client} returns the default {Client}: thin wrappers over JSON-RPC (`eth_*`) that return Ruby
45
+ # values (Integer quantities in wei, checksummed addresses, snake_case symbol keys), plus fee estimation,
46
+ # receipt polling, log fetching and stoppable `watch_*` pollers.
47
+ # - {Contract} is a class-level DSL (`abi_file`, `address`, `chain`) that generates typed read, write and
48
+ # simulate methods and decoded events from an ABI.
49
+ # - {Wallet} holds a private key and signs transactions (EIP-1559 and legacy), personal messages (EIP-191)
50
+ # and typed data (EIP-712).
51
+ # - {Connectors} carry the transport: {Connectors::Alchemy} and {Connectors::Http} for real nodes,
52
+ # {Connectors::Stub} for tests.
53
+ # - {Utils}, {Normalizer} and {Chains} are stateless helpers (unit parsing, hex handling, keccak256,
54
+ # RPC payload normalisation and the catalogue of known networks).
55
+ #
56
+ # Every error raised by the gem inherits from {BlockGiven::Error}.
57
+ #
58
+ # @example Configure once, then use the default client
33
59
  # BlockGiven.configure do |c|
34
60
  # c.connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
35
61
  # c.chain = :base
36
62
  # end
63
+ #
64
+ # BlockGiven.client.block_number # => 12_345_678
37
65
  module BlockGiven
38
66
  class << self
67
+ # Global configuration shared by the default client and by contracts without an explicit chain or connector.
68
+ #
69
+ # @return [BlockGiven::Configuration] the memoised configuration, created with defaults on first access
39
70
  def config
40
71
  @config ||= Configuration.new
41
72
  end
42
73
 
74
+ # Configure the gem and discard the memoised default client so the next {.client} call reflects the changes.
75
+ #
76
+ # @yield [config] the global configuration to mutate
77
+ # @yieldparam config [BlockGiven::Configuration]
78
+ # @return [BlockGiven::Configuration] the updated configuration
79
+ # @example
80
+ # BlockGiven.configure do |c|
81
+ # c.connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
82
+ # c.chain = :base_sepolia
83
+ # c.polling_interval = 1.0
84
+ # c.abi_path = Rails.root.join("abis")
85
+ # end
43
86
  def configure
44
87
  yield config
45
88
  @client = nil
@@ -47,22 +90,42 @@ module BlockGiven
47
90
  end
48
91
 
49
92
  # Default client built from the global configuration.
93
+ #
94
+ # Memoised until {.configure} or {.reset!} runs, or until a client is assigned through {.client=}.
95
+ #
96
+ # @return [BlockGiven::Client]
97
+ # @raise [BlockGiven::ConfigurationError] when no connector or no chain is configured
98
+ # @example
99
+ # BlockGiven.client.get_balance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") # => 1_000_000_000_000_000_000
50
100
  def client
51
101
  @client ||= Client.new
52
102
  end
53
103
 
104
+ # Replace the default client, for instance with one built on a {Connectors::Stub} in tests.
105
+ #
106
+ # @param value [BlockGiven::Client, nil] nil makes the next {.client} call rebuild one from the configuration
107
+ # @return [BlockGiven::Client, nil]
54
108
  attr_writer :client
55
109
 
56
- # Running background watchers (see BlockGiven::Watcher.find / stop / stop_all).
110
+ # Running background watchers, oldest first (see {Watcher.find}, {Watcher.stop} and {Watcher.stop_all}).
111
+ #
112
+ # @return [Array<BlockGiven::Watcher>]
57
113
  def watchers = Watcher.all
58
114
 
59
- # Forget configuration and default client, stop every watcher (useful in tests).
115
+ # Forget configuration and default client, and stop every watcher (useful in tests).
116
+ #
117
+ # Watchers are asked to stop gracefully and joined for up to one second each before the state is cleared.
118
+ #
119
+ # @return [void]
60
120
  def reset!
61
121
  Watcher.stop_all(join: 1)
62
122
  @config = nil
63
123
  @client = nil
64
124
  end
65
125
 
126
+ # Logger from the global configuration.
127
+ #
128
+ # @return [Logger] a `$stderr` logger at WARN level unless the application set one (see {Configuration#logger})
66
129
  def logger = config.logger
67
130
  end
68
131
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: block_given
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Remi Wallaere
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-11 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -24,26 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.1'
27
- - !ruby/object:Gem::Dependency
28
- name: eth
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.5'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 0.5.17
37
- type: :runtime
38
- prerelease: false
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - "~>"
42
- - !ruby/object:Gem::Version
43
- version: '0.5'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 0.5.17
47
27
  - !ruby/object:Gem::Dependency
48
28
  name: logger
49
29
  requirement: !ruby/object:Gem::Requirement
@@ -63,6 +43,7 @@ description: |
63
43
  inspired by viem: typed contract classes generated from an ABI, wallets that sign
64
44
  EIP-1559 transactions, pluggable JSON-RPC connectors (Alchemy first) and
65
45
  polling helpers for receipts, blocks and events.
46
+ Documentation: https://rubydoc.info/gems/block_given
66
47
  email:
67
48
  - remi@boleromusic.com
68
49
  executables: []
@@ -73,12 +54,20 @@ files:
73
54
  - LICENSE.txt
74
55
  - README.md
75
56
  - lib/block_given.rb
57
+ - lib/block_given/abi/codec.rb
58
+ - lib/block_given/abi/codec/decoder.rb
76
59
  - lib/block_given/abi/coder.rb
77
60
  - lib/block_given/abi/custom_error.rb
78
61
  - lib/block_given/abi/event.rb
79
62
  - lib/block_given/abi/function.rb
80
63
  - lib/block_given/abi/interface.rb
81
64
  - lib/block_given/abi/parameter.rb
65
+ - lib/block_given/abi/standards.rb
66
+ - lib/block_given/abi/standards/erc1155.rb
67
+ - lib/block_given/abi/standards/erc20.rb
68
+ - lib/block_given/abi/standards/erc4626.rb
69
+ - lib/block_given/abi/standards/erc721.rb
70
+ - lib/block_given/abi/type.rb
82
71
  - lib/block_given/chain.rb
83
72
  - lib/block_given/client.rb
84
73
  - lib/block_given/configuration.rb
@@ -87,14 +76,21 @@ files:
87
76
  - lib/block_given/connectors/http.rb
88
77
  - lib/block_given/connectors/stub.rb
89
78
  - lib/block_given/contract.rb
79
+ - lib/block_given/crypto.rb
80
+ - lib/block_given/crypto/keccak.rb
81
+ - lib/block_given/crypto/secp256k1.rb
82
+ - lib/block_given/eip712.rb
90
83
  - lib/block_given/errors.rb
91
84
  - lib/block_given/event.rb
92
85
  - lib/block_given/normalizer.rb
93
86
  - lib/block_given/poller.rb
94
87
  - lib/block_given/railtie.rb
95
88
  - lib/block_given/receipt.rb
89
+ - lib/block_given/rlp.rb
96
90
  - lib/block_given/signed_transaction.rb
97
91
  - lib/block_given/transaction.rb
92
+ - lib/block_given/transaction_envelope.rb
93
+ - lib/block_given/transaction_envelope/fields.rb
98
94
  - lib/block_given/utils.rb
99
95
  - lib/block_given/version.rb
100
96
  - lib/block_given/wallet.rb
@@ -123,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
119
  - !ruby/object:Gem::Version
124
120
  version: '0'
125
121
  requirements: []
126
- rubygems_version: 3.3.7
122
+ rubygems_version: 3.5.22
127
123
  signing_key:
128
124
  specification_version: 4
129
125
  summary: viem-inspired Ruby client for EVM smart contracts.