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
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Chains
5
+ MAINNET = Chain.new(
6
+ id: 1,
7
+ name: "Ethereum",
8
+ network: "homestead",
9
+ native_currency: NativeCurrency.new(name: "Ether", symbol: "ETH", decimals: 18),
10
+ rpc_urls: { default: { http: ["https://cloudflare-eth.com"] } },
11
+ block_explorers: { default: { name: "Etherscan", url: "https://etherscan.io" } },
12
+ testnet: false
13
+ )
14
+
15
+ SEPOLIA = Chain.new(
16
+ id: 11_155_111,
17
+ name: "Sepolia",
18
+ network: "sepolia",
19
+ native_currency: NativeCurrency.new(name: "Sepolia Ether", symbol: "ETH", decimals: 18),
20
+ rpc_urls: { default: { http: ["https://rpc.sepolia.org"] } },
21
+ block_explorers: { default: { name: "Etherscan", url: "https://sepolia.etherscan.io" } },
22
+ testnet: true
23
+ )
24
+
25
+ GOERLI = Chain.new(
26
+ id: 5,
27
+ name: "Goerli",
28
+ network: "goerli",
29
+ native_currency: NativeCurrency.new(name: "Goerli Ether", symbol: "ETH", decimals: 18),
30
+ rpc_urls: { default: { http: ["https://rpc.ankr.com/eth_goerli"] } },
31
+ block_explorers: { default: { name: "Etherscan", url: "https://goerli.etherscan.io" } },
32
+ testnet: true
33
+ )
34
+
35
+ POLYGON = Chain.new(
36
+ id: 137,
37
+ name: "Polygon",
38
+ network: "matic",
39
+ native_currency: NativeCurrency.new(name: "MATIC", symbol: "MATIC", decimals: 18),
40
+ rpc_urls: { default: { http: ["https://polygon-rpc.com"] } },
41
+ block_explorers: { default: { name: "PolygonScan", url: "https://polygonscan.com" } },
42
+ testnet: false
43
+ )
44
+
45
+ POLYGON_MUMBAI = Chain.new(
46
+ id: 80_001,
47
+ name: "Polygon Mumbai",
48
+ network: "maticmum",
49
+ native_currency: NativeCurrency.new(name: "MATIC", symbol: "MATIC", decimals: 18),
50
+ rpc_urls: { default: { http: ["https://rpc-mumbai.maticvigil.com"] } },
51
+ block_explorers: { default: { name: "PolygonScan", url: "https://mumbai.polygonscan.com" } },
52
+ testnet: true
53
+ )
54
+
55
+ OPTIMISM = Chain.new(
56
+ id: 10,
57
+ name: "Optimism",
58
+ network: "optimism",
59
+ native_currency: NativeCurrency.new(name: "Ether", symbol: "ETH", decimals: 18),
60
+ rpc_urls: { default: { http: ["https://mainnet.optimism.io"] } },
61
+ block_explorers: { default: { name: "Optimism Explorer", url: "https://optimistic.etherscan.io" } },
62
+ testnet: false
63
+ )
64
+
65
+ OPTIMISM_GOERLI = Chain.new(
66
+ id: 420,
67
+ name: "Optimism Goerli",
68
+ network: "optimism-goerli",
69
+ native_currency: NativeCurrency.new(name: "Goerli Ether", symbol: "ETH", decimals: 18),
70
+ rpc_urls: { default: { http: ["https://goerli.optimism.io"] } },
71
+ block_explorers: { default: { name: "Optimism Explorer", url: "https://goerli-optimism.etherscan.io" } },
72
+ testnet: true
73
+ )
74
+
75
+ ARBITRUM = Chain.new(
76
+ id: 42_161,
77
+ name: "Arbitrum One",
78
+ network: "arbitrum",
79
+ native_currency: NativeCurrency.new(name: "Ether", symbol: "ETH", decimals: 18),
80
+ rpc_urls: { default: { http: ["https://arb1.arbitrum.io/rpc"] } },
81
+ block_explorers: { default: { name: "Arbiscan", url: "https://arbiscan.io" } },
82
+ testnet: false
83
+ )
84
+
85
+ ARBITRUM_GOERLI = Chain.new(
86
+ id: 421_613,
87
+ name: "Arbitrum Goerli",
88
+ network: "arbitrum-goerli",
89
+ native_currency: NativeCurrency.new(name: "Arbitrum Goerli Ether", symbol: "AGOR", decimals: 18),
90
+ rpc_urls: { default: { http: ["https://goerli-rollup.arbitrum.io/rpc"] } },
91
+ block_explorers: { default: { name: "Arbiscan", url: "https://goerli.arbiscan.io" } },
92
+ testnet: true
93
+ )
94
+
95
+ BASE = Chain.new(
96
+ id: 8453,
97
+ name: "Base",
98
+ network: "base",
99
+ native_currency: NativeCurrency.new(name: "Ether", symbol: "ETH", decimals: 18),
100
+ rpc_urls: { default: { http: ["https://mainnet.base.org"] } },
101
+ block_explorers: { default: { name: "Basescan", url: "https://basescan.org" } },
102
+ testnet: false
103
+ )
104
+
105
+ BASE_GOERLI = Chain.new(
106
+ id: 84_531,
107
+ name: "Base Goerli",
108
+ network: "base-goerli",
109
+ native_currency: NativeCurrency.new(name: "Goerli Ether", symbol: "ETH", decimals: 18),
110
+ rpc_urls: { default: { http: ["https://goerli.base.org"] } },
111
+ block_explorers: { default: { name: "Basescan", url: "https://goerli.basescan.org" } },
112
+ testnet: true
113
+ )
114
+
115
+ AVALANCHE = Chain.new(
116
+ id: 43_114,
117
+ name: "Avalanche",
118
+ network: "avalanche",
119
+ native_currency: NativeCurrency.new(name: "Avalanche", symbol: "AVAX", decimals: 18),
120
+ rpc_urls: { default: { http: ["https://api.avax.network/ext/bc/C/rpc"] } },
121
+ block_explorers: { default: { name: "SnowTrace", url: "https://snowtrace.io" } },
122
+ testnet: false
123
+ )
124
+
125
+ AVALANCHE_FUJI = Chain.new(
126
+ id: 43_113,
127
+ name: "Avalanche Fuji",
128
+ network: "avalanche-fuji",
129
+ native_currency: NativeCurrency.new(name: "Avalanche", symbol: "AVAX", decimals: 18),
130
+ rpc_urls: { default: { http: ["https://api.avax-test.network/ext/bc/C/rpc"] } },
131
+ block_explorers: { default: { name: "SnowTrace", url: "https://testnet.snowtrace.io" } },
132
+ testnet: true
133
+ )
134
+
135
+ BSC = Chain.new(
136
+ id: 56,
137
+ name: "BNB Smart Chain",
138
+ network: "bsc",
139
+ native_currency: NativeCurrency.new(name: "BNB", symbol: "BNB", decimals: 18),
140
+ rpc_urls: { default: { http: ["https://bsc-dataseed.binance.org"] } },
141
+ block_explorers: { default: { name: "BscScan", url: "https://bscscan.com" } },
142
+ testnet: false
143
+ )
144
+
145
+ BSC_TESTNET = Chain.new(
146
+ id: 97,
147
+ name: "BNB Smart Chain Testnet",
148
+ network: "bsc-testnet",
149
+ native_currency: NativeCurrency.new(name: "BNB", symbol: "tBNB", decimals: 18),
150
+ rpc_urls: { default: { http: ["https://data-seed-prebsc-1-s1.binance.org:8545"] } },
151
+ block_explorers: { default: { name: "BscScan", url: "https://testnet.bscscan.com" } },
152
+ testnet: true
153
+ )
154
+
155
+ GNOSIS = Chain.new(
156
+ id: 100,
157
+ name: "Gnosis",
158
+ network: "gnosis",
159
+ native_currency: NativeCurrency.new(name: "xDAI", symbol: "xDAI", decimals: 18),
160
+ rpc_urls: { default: { http: ["https://rpc.gnosischain.com"] } },
161
+ block_explorers: { default: { name: "Gnosis Scan", url: "https://gnosisscan.io" } },
162
+ testnet: false
163
+ )
164
+
165
+ FANTOM = Chain.new(
166
+ id: 250,
167
+ name: "Fantom",
168
+ network: "fantom",
169
+ native_currency: NativeCurrency.new(name: "Fantom", symbol: "FTM", decimals: 18),
170
+ rpc_urls: { default: { http: ["https://rpc.ftm.tools"] } },
171
+ block_explorers: { default: { name: "FtmScan", url: "https://ftmscan.com" } },
172
+ testnet: false
173
+ )
174
+
175
+ CELO = Chain.new(
176
+ id: 42_220,
177
+ name: "Celo",
178
+ network: "celo",
179
+ native_currency: NativeCurrency.new(name: "CELO", symbol: "CELO", decimals: 18),
180
+ rpc_urls: { default: { http: ["https://forno.celo.org"] } },
181
+ block_explorers: { default: { name: "Celo Explorer", url: "https://explorer.celo.org" } },
182
+ testnet: false
183
+ )
184
+ end
185
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Clients
5
+ class PublicClient
6
+ include Actions::Public::GetBalance
7
+ include Actions::Public::GetBlock
8
+ include Actions::Public::GetTransaction
9
+ include Actions::Public::Call
10
+ include Actions::Public::ReadContract
11
+ include Actions::Public::GetLogs
12
+ include Actions::Public::GetGas
13
+ include Actions::Public::GetCode
14
+ include Actions::Public::Ens
15
+
16
+ attr_reader :transport, :chain
17
+
18
+ def initialize(transport:, chain: nil)
19
+ @transport = transport
20
+ @chain = chain
21
+ end
22
+
23
+ def chain_id
24
+ result = @transport.request("eth_chainId", [])
25
+ Utils::Hex.hex_to_number(result)
26
+ end
27
+
28
+ def get_network
29
+ { chain_id: chain_id, name: @chain&.name }
30
+ end
31
+
32
+ private
33
+
34
+ def stringify_keys(h)
35
+ h.transform_keys { |k| k.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase } }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Clients
5
+ class TestClient < PublicClient
6
+ def mine(blocks: 1, interval: 0)
7
+ @transport.request("anvil_mine", [
8
+ Utils::Hex.number_to_hex(blocks),
9
+ Utils::Hex.number_to_hex(interval)
10
+ ])
11
+ end
12
+
13
+ def set_balance(address:, value:)
14
+ @transport.request("anvil_setBalance", [address, Utils::Hex.number_to_hex(value)])
15
+ end
16
+
17
+ def set_code(address:, bytecode:)
18
+ @transport.request("anvil_setCode", [address, bytecode])
19
+ end
20
+
21
+ def set_storage_at(address:, slot:, value:)
22
+ @transport.request("anvil_setStorageAt", [address, slot, value])
23
+ end
24
+
25
+ def impersonate_account(address:)
26
+ @transport.request("anvil_impersonateAccount", [address])
27
+ end
28
+
29
+ def stop_impersonating_account(address:)
30
+ @transport.request("anvil_stopImpersonatingAccount", [address])
31
+ end
32
+
33
+ def snapshot
34
+ @transport.request("evm_snapshot", [])
35
+ end
36
+
37
+ def revert(id:)
38
+ @transport.request("evm_revert", [id])
39
+ end
40
+
41
+ def increase_time(seconds:)
42
+ @transport.request("evm_increaseTime", [seconds])
43
+ end
44
+
45
+ def set_next_block_timestamp(timestamp:)
46
+ @transport.request("evm_setNextBlockTimestamp", [timestamp])
47
+ end
48
+
49
+ def reset(url: nil, block_number: nil)
50
+ params = url ? [{ jsonRpcUrl: url, blockNumber: block_number }.compact] : []
51
+ @transport.request("anvil_reset", params)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Clients
5
+ class WalletClient < PublicClient
6
+ include Actions::Wallet::SendTransaction
7
+ include Actions::Wallet::SignMessage
8
+ include Actions::Wallet::WriteContract
9
+ include Actions::Wallet::SignTypedData
10
+
11
+ attr_reader :account
12
+
13
+ def initialize(transport:, chain: nil, account: nil)
14
+ super(transport: transport, chain: chain)
15
+ @account = account
16
+ end
17
+
18
+ def get_addresses
19
+ return [@account.address] if @account
20
+
21
+ @transport.request("eth_accounts", [])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ class Error < StandardError; end
5
+
6
+ class TransportError < Error; end
7
+
8
+ class HttpRequestError < TransportError
9
+ attr_reader :status, :body
10
+
11
+ def initialize(msg = nil, status: nil, body: nil)
12
+ @status = status
13
+ @body = body
14
+ super(msg)
15
+ end
16
+ end
17
+
18
+ class RpcError < Error
19
+ attr_reader :code, :data
20
+
21
+ def initialize(msg = nil, code: nil, data: nil)
22
+ @code = code
23
+ @data = data
24
+ super(msg)
25
+ end
26
+ end
27
+
28
+ class UserRejectedError < RpcError; end
29
+
30
+ class ContractFunctionExecutionError < Error
31
+ attr_reader :cause, :contract_address, :function_name, :args
32
+
33
+ def initialize(cause, contract_address: nil, function_name: nil, args: [])
34
+ @cause = cause
35
+ @contract_address = contract_address
36
+ @function_name = function_name
37
+ @args = args
38
+ super("Contract function '#{function_name}' reverted: #{cause.message}")
39
+ end
40
+ end
41
+
42
+ class AbiEncodingError < Error; end
43
+ class AbiDecodingError < Error; end
44
+
45
+ class InvalidAddressError < Error
46
+ def initialize(address)
47
+ super("Invalid Ethereum address: #{address.inspect}")
48
+ end
49
+ end
50
+
51
+ class ChainMismatchError < Error; end
52
+
53
+ class AccountRequiredError < Error
54
+ def initialize
55
+ super("No account set on WalletClient. Pass an account to the client or action.")
56
+ end
57
+ end
58
+
59
+ class BlockNotFoundError < Error; end
60
+ class TransactionNotFoundError < Error; end
61
+ class TransactionReceiptNotFoundError < Error; end
62
+ class WaitForTransactionReceiptTimeoutError < Error; end
63
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Viem
6
+ module Transports
7
+ class Base
8
+ def request(method, params = [])
9
+ raise NotImplementedError
10
+ end
11
+
12
+ private
13
+
14
+ def build_rpc_body(method, params, id = 1)
15
+ { jsonrpc: "2.0", id: id, method: method, params: params }
16
+ end
17
+
18
+ def parse_response(body)
19
+ data = body.is_a?(String) ? JSON.parse(body, symbolize_names: true) : body
20
+ raise Viem::RpcError.new(data[:error][:message], code: data[:error][:code]) if data[:error]
21
+
22
+ data[:result]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Transports
5
+ class Fallback < Base
6
+ def initialize(*transports)
7
+ raise ArgumentError, "At least one transport required" if transports.empty?
8
+
9
+ @transports = transports
10
+ end
11
+
12
+ def request(method, params = [])
13
+ last_error = nil
14
+ @transports.each do |t|
15
+ return t.request(method, params)
16
+ rescue TransportError, RpcError => e
17
+ last_error = e
18
+ next
19
+ end
20
+ raise last_error || TransportError.new("All transports failed")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Viem
7
+ module Transports
8
+ class Http < Base
9
+ attr_reader :url
10
+
11
+ def initialize(url, headers: {}, timeout: 30)
12
+ @url = url
13
+ @headers = headers
14
+ @timeout = timeout
15
+ @mutex = Mutex.new
16
+ @conn = build_connection
17
+ end
18
+
19
+ def request(method, params = [])
20
+ body = build_rpc_body(method, params)
21
+ resp = @mutex.synchronize { @conn.post("/", body.to_json) }
22
+ unless resp.success?
23
+ raise HttpRequestError.new("HTTP #{resp.status}", status: resp.status, body: resp.body)
24
+ end
25
+
26
+ parse_response(resp.body)
27
+ rescue Faraday::Error => e
28
+ raise TransportError, e.message
29
+ end
30
+
31
+ private
32
+
33
+ def build_connection
34
+ Faraday.new(url: @url) do |f|
35
+ f.headers["Content-Type"] = "application/json"
36
+ f.headers["User-Agent"] = "viem_rb/#{Viem::VERSION}"
37
+ @headers.each { |k, v| f.headers[k.to_s] = v }
38
+ f.options.timeout = @timeout
39
+ f.options.open_timeout = 10
40
+ f.adapter Faraday.default_adapter
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "websocket-client-simple"
4
+ require "json"
5
+
6
+ module Viem
7
+ module Transports
8
+ class WebSocket < Base
9
+ def initialize(url, timeout: 30)
10
+ @url = url
11
+ @timeout = timeout
12
+ @pending = {}
13
+ @mutex = Mutex.new
14
+ @id = 0
15
+ connect
16
+ end
17
+
18
+ def request(method, params = [])
19
+ id = next_id
20
+ body = build_rpc_body(method, params, id)
21
+ q = Queue.new
22
+ @mutex.synchronize { @pending[id] = q }
23
+ @ws.send(body.to_json)
24
+ result = q.pop(timeout: @timeout)
25
+ raise TransportError, "WebSocket timeout waiting for response to #{method}" if result.nil?
26
+ raise RpcError.new(result[:error][:message], code: result[:error][:code]) if result[:error]
27
+
28
+ result[:result]
29
+ end
30
+
31
+ def close
32
+ @ws.close
33
+ end
34
+
35
+ private
36
+
37
+ def next_id
38
+ @mutex.synchronize { @id += 1 }
39
+ end
40
+
41
+ def connect
42
+ url = @url
43
+ pending = @pending
44
+ mutex = @mutex
45
+
46
+ @ws = ::WebSocket::Client::Simple.connect(url) do |ws|
47
+ ws.on :message do |msg|
48
+ data = JSON.parse(msg.data, symbolize_names: true)
49
+ q = mutex.synchronize { pending.delete(data[:id]) }
50
+ q&.push(data)
51
+ end
52
+
53
+ ws.on :error do |e|
54
+ mutex.synchronize do
55
+ pending.each_value { |q| q.push({ error: { message: e.message, code: -32_000 } }) }
56
+ end
57
+ end
58
+ end
59
+
60
+ sleep 0.1 # let connection establish
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ module Viem
6
+ module Utils
7
+ module Address
8
+ def self.is_address?(value)
9
+ return false unless value.is_a?(String)
10
+
11
+ Eth::Address.new(value).valid?
12
+ rescue StandardError
13
+ false
14
+ end
15
+
16
+ def self.get_address(address)
17
+ raise InvalidAddressError, address unless is_address?(address)
18
+
19
+ Eth::Address.new(address).checksummed
20
+ end
21
+
22
+ def self.zero_address
23
+ "0x0000000000000000000000000000000000000000"
24
+ end
25
+
26
+ def self.is_zero_address?(address)
27
+ address.downcase == zero_address
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+ require "digest"
5
+
6
+ module Viem
7
+ module Utils
8
+ module Hash
9
+ def self.keccak256(data)
10
+ bytes = if data.is_a?(String) && data.start_with?("0x")
11
+ [data.delete_prefix("0x")].pack("H*")
12
+ else
13
+ data.to_s
14
+ end
15
+ "0x#{Eth::Util.keccak256(bytes).unpack1("H*")}"
16
+ end
17
+
18
+ def self.hash_message(message)
19
+ keccak256("\x19Ethereum Signed Message:\n#{message.bytesize}#{message}")
20
+ end
21
+
22
+ def self.sha256(data)
23
+ "0x#{Digest::SHA256.hexdigest(data.to_s)}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Viem
4
+ module Utils
5
+ module Hex
6
+ def self.is_hex?(value, strict: false)
7
+ return false unless value.is_a?(String)
8
+
9
+ if strict
10
+ value.match?(/\A0x[0-9a-fA-F]+\z/)
11
+ else
12
+ value.match?(/\A0x[0-9a-fA-F]*\z/)
13
+ end
14
+ end
15
+
16
+ def self.to_hex(value, size: nil)
17
+ hex = case value
18
+ when Integer
19
+ value.negative? ? twos_complement(value) : value.to_s(16)
20
+ when String
21
+ value.unpack1("H*")
22
+ when Array
23
+ value.pack("C*").unpack1("H*")
24
+ else
25
+ raise ArgumentError, "Cannot convert #{value.class} to hex"
26
+ end
27
+ hex = hex.rjust(size * 2, "0") if size
28
+ "0x#{hex}"
29
+ end
30
+
31
+ def self.hex_to_number(hex)
32
+ strip(hex).to_i(16)
33
+ end
34
+
35
+ def self.number_to_hex(num, size: nil)
36
+ to_hex(num, size: size)
37
+ end
38
+
39
+ def self.hex_to_bytes(hex)
40
+ [strip(hex)].pack("H*").bytes
41
+ end
42
+
43
+ def self.bytes_to_hex(bytes)
44
+ "0x#{bytes.pack("C*").unpack1("H*")}"
45
+ end
46
+
47
+ def self.hex_to_string(hex)
48
+ [strip(hex)].pack("H*")
49
+ end
50
+
51
+ def self.string_to_hex(str)
52
+ "0x#{str.unpack1("H*")}"
53
+ end
54
+
55
+ def self.strip(hex)
56
+ hex.delete_prefix("0x")
57
+ end
58
+
59
+ private_class_method def self.twos_complement(value)
60
+ bits = value.bit_length + 1
61
+ bits += (8 - bits % 8) if (bits % 8) != 0
62
+ (2**bits + value).to_s(16)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "bigdecimal/util"
5
+
6
+ module Viem
7
+ module Utils
8
+ module Units
9
+ WEI_PER_ETHER = 10**18
10
+ WEI_PER_GWEI = 10**9
11
+
12
+ def self.parse_ether(ether)
13
+ (BigDecimal(ether.to_s) * WEI_PER_ETHER).to_i
14
+ end
15
+
16
+ def self.format_ether(wei)
17
+ (BigDecimal(wei.to_s) / WEI_PER_ETHER).to_s("F")
18
+ end
19
+
20
+ def self.parse_gwei(gwei)
21
+ (BigDecimal(gwei.to_s) * WEI_PER_GWEI).to_i
22
+ end
23
+
24
+ def self.format_gwei(wei)
25
+ (BigDecimal(wei.to_s) / WEI_PER_GWEI).to_s("F")
26
+ end
27
+
28
+ def self.parse_units(value, decimals)
29
+ (BigDecimal(value.to_s) * 10**decimals).to_i
30
+ end
31
+
32
+ def self.format_units(value, decimals)
33
+ (BigDecimal(value.to_s) / 10**decimals).to_s("F")
34
+ end
35
+ end
36
+ end
37
+ end