viem_rb 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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE +21 -0
  4. data/README.md +365 -0
  5. data/lib/viem/abi/decoder.rb +28 -0
  6. data/lib/viem/abi/encoder.rb +48 -0
  7. data/lib/viem/abi/parse.rb +59 -0
  8. data/lib/viem/accounts/mnemonic_account.rb +34 -0
  9. data/lib/viem/accounts/private_key_account.rb +51 -0
  10. data/lib/viem/actions/public/call.rb +35 -0
  11. data/lib/viem/actions/public/ens.rb +85 -0
  12. data/lib/viem/actions/public/get_balance.rb +15 -0
  13. data/lib/viem/actions/public/get_block.rb +47 -0
  14. data/lib/viem/actions/public/get_code.rb +21 -0
  15. data/lib/viem/actions/public/get_gas.rb +33 -0
  16. data/lib/viem/actions/public/get_logs.rb +32 -0
  17. data/lib/viem/actions/public/get_transaction.rb +72 -0
  18. data/lib/viem/actions/public/read_contract.rb +41 -0
  19. data/lib/viem/actions/wallet/send_transaction.rb +92 -0
  20. data/lib/viem/actions/wallet/sign_message.rb +16 -0
  21. data/lib/viem/actions/wallet/sign_typed_data.rb +16 -0
  22. data/lib/viem/actions/wallet/write_contract.rb +48 -0
  23. data/lib/viem/chains/base.rb +21 -0
  24. data/lib/viem/chains/definitions.rb +185 -0
  25. data/lib/viem/clients/public_client.rb +39 -0
  26. data/lib/viem/clients/test_client.rb +55 -0
  27. data/lib/viem/clients/wallet_client.rb +25 -0
  28. data/lib/viem/errors.rb +63 -0
  29. data/lib/viem/transports/base.rb +26 -0
  30. data/lib/viem/transports/fallback.rb +24 -0
  31. data/lib/viem/transports/http.rb +45 -0
  32. data/lib/viem/transports/web_socket.rb +64 -0
  33. data/lib/viem/utils/address.rb +31 -0
  34. data/lib/viem/utils/hash.rb +27 -0
  35. data/lib/viem/utils/hex.rb +66 -0
  36. data/lib/viem/utils/units.rb +37 -0
  37. data/lib/viem/version.rb +5 -0
  38. data/lib/viem_rb.rb +71 -0
  39. metadata +166 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8cc9cafb976fa3b96e5dba8e7fbd21b64135eefeb0c3945333384b7ea7dfc7a
4
+ data.tar.gz: 165980b44b57a997c7ebb0f990ef9f6e10f2ffb05a4e415eac9e8895e16444f3
5
+ SHA512:
6
+ metadata.gz: 06d884e59ec32a5130816f4f6d9f5471d0cd63ba9e625ab60a7b1cd5dd31e853e420d3ae37521815529aa1b8270d4bb56c4e12eb535347e0a11a79ce895ea807
7
+ data.tar.gz: 67d8964c2dd2e5cec9e0fc4b049b90416660d6eb89bb2c48b66fdd9f3863f3708c9433fb6c848370cb6545b6e94bc0b2cc2ac26f5660d1ebe145f0911061feb1
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-01
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - `PublicClient` with full read-only Ethereum JSON-RPC actions
14
+ - `WalletClient` extending `PublicClient` with signing and transaction submission
15
+ - `TestClient` extending `PublicClient` with Anvil/Hardhat test node helpers
16
+ - HTTP transport (`Viem::Transports::Http`) using Faraday, thread-safe with Mutex
17
+ - WebSocket transport (`Viem::Transports::WebSocket`) using websocket-client-simple
18
+ - Fallback transport (`Viem::Transports::Fallback`) for automatic failover
19
+ - `PrivateKeyAccount` for local key management and signing
20
+ - ABI encoding/decoding via the `eth` gem
21
+ - Human-readable ABI parser (`Viem::Abi::Parse`)
22
+ - Hex utilities (`Viem::Utils::Hex`)
23
+ - Unit conversion utilities (`Viem::Utils::Units`) — parse/format Ether, Gwei, and arbitrary decimals
24
+ - Address utilities (`Viem::Utils::Address`) — validation and EIP-55 checksum
25
+ - Hash utilities (`Viem::Utils::Hash`) — keccak256, hash_message, sha256
26
+ - Full error hierarchy under `Viem::Error`
27
+ - 18 pre-configured chain definitions (Mainnet, Sepolia, Polygon, Optimism, Arbitrum, Base, Avalanche, BSC, Gnosis, Fantom, Celo, and their testnets)
28
+ - ENS resolution (`get_ens_address`, `get_ens_name`)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Noryk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,365 @@
1
+ # viem_rb
2
+
3
+ A Ruby/Rails adaptation of [viem](https://viem.sh) — the TypeScript Ethereum library. Provides Ethereum clients, ABI encoding/decoding, account management, and utilities for Ruby 2.7+ and Rails 7+ applications.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "viem_rb"
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```bash
16
+ gem install viem_rb
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Public Client (read-only)
22
+
23
+ ```ruby
24
+ require "viem_rb"
25
+
26
+ # Create a transport
27
+ transport = Viem.http("https://cloudflare-eth.com")
28
+
29
+ # Create a public client
30
+ client = Viem.create_public_client(
31
+ transport: transport,
32
+ chain: Viem::MAINNET
33
+ )
34
+
35
+ # Read the chain ID
36
+ client.chain_id # => 1
37
+
38
+ # Get ETH balance
39
+ balance = client.get_balance(address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
40
+ # => 1000000000000000000 (in wei)
41
+
42
+ Viem::Utils::Units.format_ether(balance) # => "1.0"
43
+
44
+ # Get current block
45
+ block = client.get_block
46
+ # => { number: 18_000_000, hash: "0x...", timestamp: 1_700_000_000, ... }
47
+
48
+ # Get block by number
49
+ client.get_block(block_number: 17_000_000)
50
+
51
+ # Get transaction
52
+ tx = client.get_transaction(hash: "0xabc...")
53
+
54
+ # Get transaction receipt
55
+ receipt = client.get_transaction_receipt(hash: "0xabc...")
56
+
57
+ # Read a contract
58
+ erc20_abi = [
59
+ { "name" => "balanceOf", "type" => "function",
60
+ "inputs" => [{ "type" => "address", "name" => "account" }],
61
+ "outputs" => [{ "type" => "uint256", "name" => "" }],
62
+ "stateMutability" => "view" }
63
+ ]
64
+
65
+ balance = client.read_contract(
66
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC
67
+ abi: erc20_abi,
68
+ function_name: "balanceOf",
69
+ args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]
70
+ )
71
+ ```
72
+
73
+ ### Wallet Client (read + write)
74
+
75
+ ```ruby
76
+ # Create an account from a private key
77
+ account = Viem.private_key_to_account("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
78
+ # => #<Viem::Accounts::PrivateKeyAccount address="0xf39Fd6e...">
79
+
80
+ transport = Viem.http("https://mainnet.example.com")
81
+
82
+ client = Viem.create_wallet_client(
83
+ transport: transport,
84
+ chain: Viem::MAINNET,
85
+ account: account
86
+ )
87
+
88
+ # Sign a message
89
+ sig = client.sign_message(message: "Hello, Ethereum!")
90
+ # => "0x..."
91
+
92
+ # Send ETH
93
+ tx_hash = client.send_transaction(
94
+ to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
95
+ value: Viem::Utils::Units.parse_ether("0.01")
96
+ )
97
+
98
+ # Write to a contract
99
+ erc20_abi = [...] # full ABI
100
+
101
+ tx_hash = client.write_contract(
102
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
103
+ abi: erc20_abi,
104
+ function_name: "transfer",
105
+ args: ["0xRecipient...", 1_000_000] # 1 USDC (6 decimals)
106
+ )
107
+
108
+ # Wait for receipt
109
+ receipt = client.wait_for_transaction_receipt(hash: tx_hash, timeout: 60)
110
+ ```
111
+
112
+ ### WebSocket Transport
113
+
114
+ ```ruby
115
+ transport = Viem.web_socket("wss://mainnet.infura.io/ws/v3/YOUR_KEY")
116
+ client = Viem.create_public_client(transport: transport)
117
+ ```
118
+
119
+ ### Fallback Transport
120
+
121
+ ```ruby
122
+ transport = Viem.fallback(
123
+ Viem.http("https://primary-rpc.com"),
124
+ Viem.http("https://backup-rpc.com")
125
+ )
126
+ client = Viem.create_public_client(transport: transport)
127
+ ```
128
+
129
+ ### Test Client (Anvil / Hardhat)
130
+
131
+ ```ruby
132
+ transport = Viem.http("http://localhost:8545")
133
+ test_client = Viem.create_test_client(transport: transport)
134
+
135
+ test_client.mine(blocks: 5)
136
+ test_client.set_balance(address: "0x...", value: Viem::Utils::Units.parse_ether("100"))
137
+ test_client.impersonate_account(address: "0x...")
138
+
139
+ id = test_client.snapshot
140
+ # ... do stuff ...
141
+ test_client.revert(id: id)
142
+ ```
143
+
144
+ ## Actions Reference
145
+
146
+ ### Public Client Actions
147
+
148
+ | Method | Description |
149
+ |--------|-------------|
150
+ | `get_balance(address:, block_tag: "latest")` | Get ETH balance in wei |
151
+ | `get_block(block_number:, block_hash:, block_tag:, include_transactions:)` | Get block by number, hash, or tag |
152
+ | `get_block_number` | Get current block number |
153
+ | `get_transaction(hash:)` | Get transaction by hash |
154
+ | `get_transaction_receipt(hash:)` | Get receipt by tx hash |
155
+ | `get_transaction_count(address:, block_tag:)` | Get nonce for address |
156
+ | `wait_for_transaction_receipt(hash:, poll_interval:, timeout:)` | Poll for receipt |
157
+ | `call(to:, data:, from:, value:, gas:, block_tag:)` | Execute eth_call |
158
+ | `estimate_gas(to:, from:, data:, value:)` | Estimate gas for a tx |
159
+ | `read_contract(address:, abi:, function_name:, args:, block_tag:)` | Read a contract function |
160
+ | `simulate_contract(...)` | Simulate a state-changing function |
161
+ | `get_logs(address:, event:, args:, from_block:, to_block:)` | Fetch event logs |
162
+ | `get_gas_price` | Get current gas price in wei |
163
+ | `get_fee_history(block_count:, newest_block:, reward_percentiles:)` | Get EIP-1559 fee history |
164
+ | `get_max_priority_fee_per_gas` | Get suggested priority fee |
165
+ | `get_code(address:, block_tag:)` | Get contract bytecode |
166
+ | `get_storage_at(address:, slot:, block_tag:)` | Read a storage slot |
167
+ | `get_ens_address(name:)` | Resolve ENS name to address |
168
+ | `get_ens_name(address:)` | Reverse ENS lookup |
169
+ | `chain_id` | Get chain ID |
170
+ | `get_network` | Get network info |
171
+
172
+ ### Wallet Client Actions
173
+
174
+ | Method | Description |
175
+ |--------|-------------|
176
+ | `send_transaction(to:, value:, data:, gas:, ...)` | Sign and send a transaction |
177
+ | `sign_message(message:, account:)` | Sign a personal message (EIP-191) |
178
+ | `sign_typed_data(domain:, types:, primary_type:, message:, account:)` | Sign EIP-712 typed data |
179
+ | `write_contract(address:, abi:, function_name:, args:, ...)` | Call a state-changing contract function |
180
+ | `deploy_contract(abi:, bytecode:, args:, ...)` | Deploy a contract |
181
+ | `get_addresses` | List accounts |
182
+
183
+ ### Test Client Actions
184
+
185
+ | Method | Description |
186
+ |--------|-------------|
187
+ | `mine(blocks:, interval:)` | Mine blocks |
188
+ | `set_balance(address:, value:)` | Set ETH balance |
189
+ | `set_code(address:, bytecode:)` | Set contract code |
190
+ | `set_storage_at(address:, slot:, value:)` | Write a storage slot |
191
+ | `impersonate_account(address:)` | Impersonate any address |
192
+ | `stop_impersonating_account(address:)` | Stop impersonation |
193
+ | `snapshot` | Take EVM snapshot |
194
+ | `revert(id:)` | Revert to snapshot |
195
+ | `increase_time(seconds:)` | Advance time |
196
+ | `set_next_block_timestamp(timestamp:)` | Set next block's timestamp |
197
+ | `reset(url:, block_number:)` | Reset to a forked state |
198
+
199
+ ## Utilities
200
+
201
+ ### Hex
202
+
203
+ ```ruby
204
+ Viem::Utils::Hex.to_hex(255) # => "0xff"
205
+ Viem::Utils::Hex.hex_to_number("0xff") # => 255
206
+ Viem::Utils::Hex.is_hex?("0xabc") # => true
207
+ Viem::Utils::Hex.string_to_hex("Hi") # => "0x4869"
208
+ Viem::Utils::Hex.hex_to_string("0x4869") # => "Hi"
209
+ ```
210
+
211
+ ### Units
212
+
213
+ ```ruby
214
+ Viem::Utils::Units.parse_ether("1") # => 1000000000000000000
215
+ Viem::Utils::Units.format_ether(1_000_000_000_000_000_000) # => "1.0"
216
+ Viem::Utils::Units.parse_gwei("10") # => 10000000000
217
+ Viem::Utils::Units.parse_units("1", 6) # => 1000000 (USDC)
218
+ ```
219
+
220
+ ### Address
221
+
222
+ ```ruby
223
+ Viem::Utils::Address.is_address?("0x...") # => true/false
224
+ Viem::Utils::Address.get_address("0x...") # => checksummed address
225
+ Viem::Utils::Address.zero_address # => "0x000...000"
226
+ ```
227
+
228
+ ### Hash / Crypto
229
+
230
+ ```ruby
231
+ Viem::Utils::Hash.keccak256("hello") # => "0x1c8aff9..."
232
+ Viem::Utils::Hash.hash_message("Hello!") # => EIP-191 hash
233
+ Viem::Utils::Hash.sha256("data") # => "0x..."
234
+ ```
235
+
236
+ ### ABI
237
+
238
+ ```ruby
239
+ # Encode
240
+ Viem::Abi::Encoder.encode_abi_parameters(["uint256", "address"], [100, "0x..."])
241
+ Viem::Abi::Encoder.encode_function_data(abi_item, args: [...])
242
+ Viem::Abi::Encoder.get_selector(abi_item) # => "0x70a08231"
243
+
244
+ # Decode
245
+ Viem::Abi::Decoder.decode_abi_parameters(["uint256"], "0x...")
246
+ Viem::Abi::Decoder.decode_function_result(abi_item, "0x...")
247
+
248
+ # Parse human-readable ABI
249
+ Viem::Abi::Parse.parse_abi([
250
+ "function balanceOf(address account) view returns (uint256)",
251
+ "event Transfer(address indexed from, address indexed to, uint256 value)"
252
+ ])
253
+ ```
254
+
255
+ ## Chains
256
+
257
+ All chains are accessible as constants:
258
+
259
+ ```ruby
260
+ Viem::MAINNET # Ethereum Mainnet (id: 1)
261
+ Viem::SEPOLIA # Sepolia Testnet (id: 11155111)
262
+ Viem::GOERLI # Goerli Testnet (id: 5)
263
+ Viem::POLYGON # Polygon (id: 137)
264
+ Viem::POLYGON_MUMBAI # Polygon Mumbai (id: 80001)
265
+ Viem::OPTIMISM # Optimism (id: 10)
266
+ Viem::OPTIMISM_GOERLI # Optimism Goerli (id: 420)
267
+ Viem::ARBITRUM # Arbitrum One (id: 42161)
268
+ Viem::ARBITRUM_GOERLI # Arbitrum Goerli (id: 421613)
269
+ Viem::BASE # Base (id: 8453)
270
+ Viem::BASE_GOERLI # Base Goerli (id: 84531)
271
+ Viem::AVALANCHE # Avalanche C-Chain (id: 43114)
272
+ Viem::AVALANCHE_FUJI # Avalanche Fuji (id: 43113)
273
+ Viem::BSC # BNB Smart Chain (id: 56)
274
+ Viem::BSC_TESTNET # BSC Testnet (id: 97)
275
+ Viem::GNOSIS # Gnosis (id: 100)
276
+ Viem::FANTOM # Fantom (id: 250)
277
+ Viem::CELO # Celo (id: 42220)
278
+ ```
279
+
280
+ Chain objects expose:
281
+
282
+ ```ruby
283
+ Viem::MAINNET.id # => 1
284
+ Viem::MAINNET.name # => "Ethereum"
285
+ Viem::MAINNET.rpc_url # => "https://cloudflare-eth.com"
286
+ Viem::MAINNET.testnet? # => false
287
+ Viem::SEPOLIA.testnet? # => true
288
+ ```
289
+
290
+ ## Error Handling
291
+
292
+ ```ruby
293
+ begin
294
+ client.get_balance(address: "invalid")
295
+ rescue Viem::InvalidAddressError => e
296
+ puts e.message # "Invalid Ethereum address: \"invalid\""
297
+ rescue Viem::RpcError => e
298
+ puts "RPC error #{e.code}: #{e.message}"
299
+ rescue Viem::HttpRequestError => e
300
+ puts "HTTP #{e.status}: #{e.body}"
301
+ rescue Viem::ContractFunctionExecutionError => e
302
+ puts "Contract error in #{e.function_name}: #{e.cause.message}"
303
+ rescue Viem::TransportError => e
304
+ puts "Transport failed: #{e.message}"
305
+ rescue Viem::Error => e
306
+ puts "Viem error: #{e.message}"
307
+ end
308
+ ```
309
+
310
+ ### Error Hierarchy
311
+
312
+ ```
313
+ Viem::Error
314
+ Viem::TransportError
315
+ Viem::HttpRequestError (.status, .body)
316
+ Viem::RpcError (.code, .data)
317
+ Viem::UserRejectedError
318
+ Viem::ContractFunctionExecutionError (.cause, .contract_address, .function_name, .args)
319
+ Viem::AbiEncodingError
320
+ Viem::AbiDecodingError
321
+ Viem::InvalidAddressError
322
+ Viem::ChainMismatchError
323
+ Viem::AccountRequiredError
324
+ Viem::BlockNotFoundError
325
+ Viem::TransactionNotFoundError
326
+ Viem::TransactionReceiptNotFoundError
327
+ Viem::WaitForTransactionReceiptTimeoutError
328
+ ```
329
+
330
+ ## Rails Integration
331
+
332
+ Add to `config/initializers/viem.rb`:
333
+
334
+ ```ruby
335
+ require "viem_rb"
336
+
337
+ ETHEREUM_CLIENT = Viem.create_public_client(
338
+ transport: Viem.http(ENV.fetch("ETH_RPC_URL", "https://cloudflare-eth.com")),
339
+ chain: Viem::MAINNET
340
+ )
341
+ ```
342
+
343
+ Then in your models or services:
344
+
345
+ ```ruby
346
+ class EthereumService
347
+ def self.balance_of(address)
348
+ ETHEREUM_CLIENT.get_balance(address: address)
349
+ end
350
+ end
351
+ ```
352
+
353
+ ## Contributing
354
+
355
+ 1. Fork the repository
356
+ 2. Create your feature branch: `git checkout -b feature/my-feature`
357
+ 3. Run the test suite: `bundle exec rspec`
358
+ 4. Run the linter: `bundle exec rubocop`
359
+ 5. Commit your changes: `git commit -m "Add my feature"`
360
+ 6. Push to the branch: `git push origin feature/my-feature`
361
+ 7. Create a Pull Request
362
+
363
+ ## License
364
+
365
+ MIT License. Copyright 2025 Noryk. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ module Viem
6
+ module Abi
7
+ module Decoder
8
+ def self.decode_abi_parameters(types, data)
9
+ data = data.delete_prefix("0x")
10
+ Eth::Abi.decode(types, [data].pack("H*"))
11
+ rescue Eth::Abi::DecodingError => e
12
+ raise AbiDecodingError, e.message
13
+ end
14
+
15
+ def self.decode_function_result(abi_item, data)
16
+ types = (abi_item["outputs"] || []).map { |o| o["type"] }
17
+ result = decode_abi_parameters(types, data)
18
+ # Return single value if one output, array otherwise
19
+ result.length == 1 ? result.first : result
20
+ end
21
+
22
+ def self.decode_error_result(abi_item, data)
23
+ types = (abi_item["inputs"] || []).map { |i| i["type"] }
24
+ decode_abi_parameters(types, data[10..]) # skip 4-byte selector
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ module Viem
6
+ module Abi
7
+ module Encoder
8
+ def self.encode_abi_parameters(types, values)
9
+ encoded = Eth::Abi.encode(types, values)
10
+ "0x#{encoded.unpack1("H*")}"
11
+ rescue Eth::Abi::EncodingError => e
12
+ raise AbiEncodingError, e.message
13
+ end
14
+
15
+ def self.encode_function_data(abi_item, args: [])
16
+ sig = function_signature(abi_item)
17
+ selector = Eth::Util.keccak256(sig)[0, 4]
18
+ types = (abi_item["inputs"] || []).map { |i| i["type"] }
19
+ encoded = Eth::Abi.encode(types, args)
20
+ "0x#{selector.unpack1("H*")}#{encoded.unpack1("H*")}"
21
+ rescue Eth::Abi::EncodingError => e
22
+ raise AbiEncodingError, e.message
23
+ end
24
+
25
+ def self.encode_deploy_data(bytecode, abi_item = nil, args: [])
26
+ bytecode = bytecode.delete_prefix("0x")
27
+ return "0x#{bytecode}" if abi_item.nil? || args.empty?
28
+
29
+ types = (abi_item["inputs"] || []).map { |i| i["type"] }
30
+ encoded = Eth::Abi.encode(types, args)
31
+ "0x#{bytecode}#{encoded.unpack1("H*")}"
32
+ rescue Eth::Abi::EncodingError => e
33
+ raise AbiEncodingError, e.message
34
+ end
35
+
36
+ def self.get_selector(abi_item)
37
+ sig = function_signature(abi_item)
38
+ "0x#{Eth::Util.keccak256(sig)[0, 4].unpack1("H*")}"
39
+ end
40
+
41
+ def self.function_signature(abi_item)
42
+ inputs = (abi_item["inputs"] || []).map { |i| i["type"] }.join(",")
43
+ "#{abi_item["name"]}(#{inputs})"
44
+ end
45
+ private_class_method :function_signature
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Abi
5
+ module Parse
6
+ TYPE_REGEX = /^(function|event|error|constructor|fallback|receive)\s+/
7
+
8
+ def self.parse_abi(signatures)
9
+ signatures.map { |sig| parse_abi_item(sig) }
10
+ end
11
+
12
+ def self.parse_abi_item(sig)
13
+ sig = sig.strip
14
+ kind = case sig
15
+ when /^function / then "function"
16
+ when /^event / then "event"
17
+ when /^error / then "error"
18
+ when /^constructor/ then "constructor"
19
+ else "function"
20
+ end
21
+
22
+ sig = sig.sub(TYPE_REGEX, "")
23
+ name, rest = sig.split("(", 2)
24
+ args_str, output_str = rest&.split(")")
25
+ inputs = parse_params(args_str || "")
26
+ outputs = parse_outputs(output_str || "")
27
+
28
+ item = { "type" => kind, "name" => name&.strip, "inputs" => inputs }
29
+ item["outputs"] = outputs if kind == "function"
30
+ item["stateMutability"] = detect_mutability(sig)
31
+ item
32
+ end
33
+
34
+ private_class_method def self.parse_params(str)
35
+ return [] if str.nil? || str.strip.empty?
36
+
37
+ str.split(",").map.with_index do |p, i|
38
+ parts = p.strip.split(/\s+/)
39
+ { "type" => parts[0], "name" => parts[1] || "arg#{i}" }
40
+ end
41
+ end
42
+
43
+ private_class_method def self.parse_outputs(str)
44
+ return [] if str.nil? || str.strip.empty?
45
+
46
+ cleaned = str.sub(/\s*returns?\s*\(/, "").sub(/\)\s*$/, "")
47
+ parse_params(cleaned)
48
+ end
49
+
50
+ private_class_method def self.detect_mutability(sig)
51
+ return "view" if sig.include?("view")
52
+ return "pure" if sig.include?("pure")
53
+ return "payable" if sig.include?("payable")
54
+
55
+ "nonpayable"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ module Viem
6
+ module Accounts
7
+ class MnemonicAccount < PrivateKeyAccount
8
+ attr_reader :mnemonic, :path
9
+
10
+ def initialize(mnemonic, path: "m/44'/60'/0'/0/0")
11
+ @mnemonic = mnemonic
12
+ @path = path
13
+ private_key = derive_private_key(mnemonic, path)
14
+ super(private_key)
15
+ end
16
+
17
+ private
18
+
19
+ def derive_private_key(mnemonic, path)
20
+ # eth gem >= 0.5.10 supports HD wallets via Eth::Key::HD
21
+ if defined?(Eth::Key::HD)
22
+ seed = Eth::Key::HD.mnemonic_to_seed(mnemonic)
23
+ master = Eth::Key::HD.from_seed(seed)
24
+ child = master.derive(path)
25
+ child.private_hex
26
+ else
27
+ raise NotImplementedError,
28
+ "Mnemonic accounts require eth gem >= 0.5.10 with HD wallet support. " \
29
+ "Use Viem.private_key_to_account instead."
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ module Viem
6
+ module Accounts
7
+ class PrivateKeyAccount
8
+ attr_reader :address, :type
9
+
10
+ def initialize(private_key)
11
+ private_key = private_key.delete_prefix("0x")
12
+ @key = Eth::Key.new(priv: private_key)
13
+ @address = Eth::Address.new(@key.address.to_s).checksummed
14
+ @type = :local
15
+ end
16
+
17
+ def sign_message(message)
18
+ prefixed = "\x19Ethereum Signed Message:\n#{message.bytesize}#{message}"
19
+ hash = Eth::Util.keccak256(prefixed)
20
+ sig = @key.sign(hash)
21
+ "0x#{sig.unpack1("H*")}"
22
+ end
23
+
24
+ def sign_transaction(tx_hash)
25
+ @key.sign(tx_hash)
26
+ end
27
+
28
+ def sign_typed_data(domain, types, value)
29
+ # EIP-712 encoding
30
+ encoded = Eth::Eip712.encode(domain, types, value)
31
+ hash = Eth::Util.keccak256(encoded)
32
+ sig = @key.sign(hash)
33
+ "0x#{sig.unpack1("H*")}"
34
+ rescue StandardError => e
35
+ raise Error, "Failed to sign typed data: #{e.message}"
36
+ end
37
+
38
+ def public_key
39
+ @key.public_hex
40
+ end
41
+
42
+ def private_key
43
+ "0x#{@key.private_hex}"
44
+ end
45
+
46
+ def to_h
47
+ { address: @address, type: @type }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Actions
5
+ module Public
6
+ module Call
7
+ def call(to:, data: nil, from: nil, value: nil, gas: nil, block_tag: "latest")
8
+ params = {}
9
+ params[:to] = to if to
10
+ params[:from] = from if from
11
+ params[:data] = data if data
12
+ params[:value] = Utils::Hex.number_to_hex(value) if value
13
+ params[:gas] = Utils::Hex.number_to_hex(gas) if gas
14
+ @transport.request("eth_call", [stringify_keys(params), block_tag.to_s])
15
+ end
16
+
17
+ def estimate_gas(to:, from: nil, data: nil, value: nil)
18
+ params = {}
19
+ params[:to] = to if to
20
+ params[:from] = from if from
21
+ params[:data] = data if data
22
+ params[:value] = Utils::Hex.number_to_hex(value) if value
23
+ result = @transport.request("eth_estimateGas", [stringify_keys(params)])
24
+ Utils::Hex.hex_to_number(result)
25
+ end
26
+
27
+ private
28
+
29
+ def stringify_keys(h)
30
+ h.transform_keys { |k| k.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase } }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end