block_given 0.1.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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockGiven
4
+ # A private key + (optional) client, able to sign and send transactions
5
+ # (viem's WalletClient + Account).
6
+ #
7
+ # wallet = BlockGiven::Wallet.new(private_key: ENV["PRIVATE_KEY"])
8
+ # wallet.address
9
+ # wallet.send_transaction(to: "0x...", value: BlockGiven::Utils.parse_ether("0.01")).wait
10
+ class Wallet
11
+ attr_reader :address
12
+
13
+ def self.generate(client: nil)
14
+ new(private_key: Eth::Key.new.private_hex, client: client)
15
+ end
16
+
17
+ def initialize(private_key:, client: nil)
18
+ hex = Utils.strip_hex(private_key.to_s.strip)
19
+ raise InvalidArgumentError, "private key must be 32 bytes hex" unless hex.match?(/\A[0-9a-fA-F]{64}\z/)
20
+
21
+ @key = Eth::Key.new(priv: hex)
22
+ @address = @key.address.checksummed
23
+ @client = client
24
+ end
25
+
26
+ def private_key = Utils.prefix_hex(@key.private_hex)
27
+ def public_key = Utils.prefix_hex(@key.public_hex)
28
+
29
+ def client = @client || BlockGiven.client
30
+ def with_client(client) = self.class.new(private_key: private_key, client: client)
31
+ def chain = client.chain
32
+
33
+ def balance(block: :latest) = client.get_balance(address, block: block)
34
+ def nonce(block: :pending) = client.get_transaction_count(address, block: block)
35
+
36
+ # EIP-191 personal_sign. Returns the 65-byte signature as hex.
37
+ def sign_message(message)
38
+ Utils.prefix_hex(@key.personal_sign(message))
39
+ end
40
+
41
+ # EIP-712 typed data (Hash with :types, :primaryType, :domain, :message).
42
+ def sign_typed_data(typed_data)
43
+ Utils.prefix_hex(@key.sign_typed_data(typed_data))
44
+ end
45
+
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.
50
+ def signed_transaction(**params)
51
+ 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)
55
+ end
56
+
57
+ # Signs and returns the raw tx hex.
58
+ def sign_transaction(**params) = signed_transaction(**params).raw
59
+
60
+ # Signs and broadcasts. Returns a BlockGiven::Transaction.
61
+ def send_transaction(**params) = signed_transaction(**params).broadcast
62
+
63
+ # Resolves every missing field (nonce, gas, fees, chain id) without signing.
64
+ def prepare_transaction(to: nil, value: 0, data: nil, gas: nil, nonce: nil, max_fee_per_gas: nil,
65
+ max_priority_fee_per_gas: nil, gas_price: nil, chain_id: nil, access_list: nil)
66
+ to = Utils.checksum_address(to) if to
67
+ value = coerce_wei(value)
68
+ data = data.nil? || data.empty? ? "" : Utils.prefix_hex(data)
69
+
70
+ params = {
71
+ from: address, to: to, value: value, data: data,
72
+ chain_id: chain_id || client.chain.id,
73
+ nonce: nonce || self.nonce,
74
+ gas: gas || estimate_gas(to: to, data: data, value: value),
75
+ access_list: access_list
76
+ }
77
+
78
+ if gas_price
79
+ params[:gas_price] = gas_price
80
+ else
81
+ fees = if max_fee_per_gas && max_priority_fee_per_gas
82
+ { max_fee_per_gas: max_fee_per_gas, max_priority_fee_per_gas: max_priority_fee_per_gas }
83
+ else
84
+ estimated = client.estimate_fees_per_gas
85
+ { max_fee_per_gas: max_fee_per_gas || estimated[:max_fee_per_gas],
86
+ max_priority_fee_per_gas: max_priority_fee_per_gas || estimated[:max_priority_fee_per_gas] }
87
+ end
88
+ params.merge!(fees)
89
+ end
90
+ params
91
+ end
92
+
93
+ def ==(other) = other.is_a?(Wallet) && other.address == address
94
+ alias eql? ==
95
+ def hash = address.hash
96
+ def to_s = address
97
+ def inspect = "#<BlockGiven::Wallet #{address}>"
98
+
99
+ private
100
+
101
+ def estimate_gas(to:, data:, value:)
102
+ raise InvalidArgumentError, "contract creation requires an explicit gas: value" if to.nil?
103
+
104
+ estimate = client.estimate_gas(to: to, data: data, from: address, value: value)
105
+ (estimate * BlockGiven.config.gas_multiplier).ceil
106
+ end
107
+
108
+ def coerce_wei(value)
109
+ case value
110
+ when nil then 0
111
+ when Integer then value
112
+ when Float, BigDecimal, Rational
113
+ unless value == value.to_i
114
+ raise InvalidArgumentError,
115
+ "value must be an integer amount of wei (use BlockGiven::Utils.parse_ether)"
116
+ end
117
+
118
+ value.to_i
119
+ when String then Utils.hex?(value) ? Utils.hex_to_int(value) : Integer(value, 10)
120
+ else raise InvalidArgumentError, "invalid value: #{value.inspect}"
121
+ end
122
+ 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
+ end
137
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ require_relative "block_given/version"
6
+ require_relative "block_given/errors"
7
+ require_relative "block_given/utils"
8
+ require_relative "block_given/chain"
9
+ require_relative "block_given/configuration"
10
+ require_relative "block_given/connectors/base"
11
+ require_relative "block_given/connectors/http"
12
+ require_relative "block_given/connectors/alchemy"
13
+ require_relative "block_given/connectors/stub"
14
+ require_relative "block_given/normalizer"
15
+ require_relative "block_given/poller"
16
+ require_relative "block_given/receipt"
17
+ require_relative "block_given/transaction"
18
+ require_relative "block_given/signed_transaction"
19
+ require_relative "block_given/client"
20
+ require_relative "block_given/wallet"
21
+ require_relative "block_given/abi/parameter"
22
+ require_relative "block_given/abi/coder"
23
+ require_relative "block_given/abi/function"
24
+ require_relative "block_given/abi/event"
25
+ require_relative "block_given/abi/custom_error"
26
+ require_relative "block_given/abi/interface"
27
+ require_relative "block_given/event"
28
+ require_relative "block_given/contract"
29
+ require_relative "block_given/railtie" if defined?(Rails::Railtie)
30
+
31
+ # viem-inspired toolkit to read from and write to EVM smart contracts.
32
+ #
33
+ # BlockGiven.configure do |c|
34
+ # c.connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
35
+ # c.chain = :base
36
+ # end
37
+ module BlockGiven
38
+ class << self
39
+ def config
40
+ @config ||= Configuration.new
41
+ end
42
+
43
+ def configure
44
+ yield config
45
+ @client = nil
46
+ config
47
+ end
48
+
49
+ # Default client built from the global configuration.
50
+ def client
51
+ @client ||= Client.new
52
+ end
53
+
54
+ attr_writer :client
55
+
56
+ # Running background watchers (see BlockGiven::Watcher.find / stop / stop_all).
57
+ def watchers = Watcher.all
58
+
59
+ # Forget configuration and default client, stop every watcher (useful in tests).
60
+ def reset!
61
+ Watcher.stop_all(join: 1)
62
+ @config = nil
63
+ @client = nil
64
+ end
65
+
66
+ def logger = config.logger
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: block_given
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Remi Wallaere
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
48
+ name: logger
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.5'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.5'
61
+ description: |
62
+ BlockGiven lets you talk to EVM smart contracts from Ruby with a small, explicit API
63
+ inspired by viem: typed contract classes generated from an ABI, wallets that sign
64
+ EIP-1559 transactions, pluggable JSON-RPC connectors (Alchemy first) and
65
+ polling helpers for receipts, blocks and events.
66
+ email:
67
+ - remi@boleromusic.com
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - CHANGELOG.md
73
+ - LICENSE.txt
74
+ - README.md
75
+ - lib/block_given.rb
76
+ - lib/block_given/abi/coder.rb
77
+ - lib/block_given/abi/custom_error.rb
78
+ - lib/block_given/abi/event.rb
79
+ - lib/block_given/abi/function.rb
80
+ - lib/block_given/abi/interface.rb
81
+ - lib/block_given/abi/parameter.rb
82
+ - lib/block_given/chain.rb
83
+ - lib/block_given/client.rb
84
+ - lib/block_given/configuration.rb
85
+ - lib/block_given/connectors/alchemy.rb
86
+ - lib/block_given/connectors/base.rb
87
+ - lib/block_given/connectors/http.rb
88
+ - lib/block_given/connectors/stub.rb
89
+ - lib/block_given/contract.rb
90
+ - lib/block_given/errors.rb
91
+ - lib/block_given/event.rb
92
+ - lib/block_given/normalizer.rb
93
+ - lib/block_given/poller.rb
94
+ - lib/block_given/railtie.rb
95
+ - lib/block_given/receipt.rb
96
+ - lib/block_given/signed_transaction.rb
97
+ - lib/block_given/transaction.rb
98
+ - lib/block_given/utils.rb
99
+ - lib/block_given/version.rb
100
+ - lib/block_given/wallet.rb
101
+ homepage: https://github.com/Bolero-Music/block_given
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ homepage_uri: https://github.com/Bolero-Music/block_given
106
+ source_code_uri: https://github.com/Bolero-Music/block_given
107
+ changelog_uri: https://github.com/Bolero-Music/block_given/blob/main/CHANGELOG.md
108
+ bug_tracker_uri: https://github.com/Bolero-Music/block_given/issues
109
+ documentation_uri: https://rubydoc.info/gems/block_given
110
+ rubygems_mfa_required: 'true'
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.1.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.3.7
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: viem-inspired Ruby client for EVM smart contracts.
130
+ test_files: []