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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +365 -0
- data/lib/viem/abi/decoder.rb +28 -0
- data/lib/viem/abi/encoder.rb +48 -0
- data/lib/viem/abi/parse.rb +59 -0
- data/lib/viem/accounts/mnemonic_account.rb +34 -0
- data/lib/viem/accounts/private_key_account.rb +51 -0
- data/lib/viem/actions/public/call.rb +35 -0
- data/lib/viem/actions/public/ens.rb +85 -0
- data/lib/viem/actions/public/get_balance.rb +15 -0
- data/lib/viem/actions/public/get_block.rb +47 -0
- data/lib/viem/actions/public/get_code.rb +21 -0
- data/lib/viem/actions/public/get_gas.rb +33 -0
- data/lib/viem/actions/public/get_logs.rb +32 -0
- data/lib/viem/actions/public/get_transaction.rb +72 -0
- data/lib/viem/actions/public/read_contract.rb +41 -0
- data/lib/viem/actions/wallet/send_transaction.rb +92 -0
- data/lib/viem/actions/wallet/sign_message.rb +16 -0
- data/lib/viem/actions/wallet/sign_typed_data.rb +16 -0
- data/lib/viem/actions/wallet/write_contract.rb +48 -0
- data/lib/viem/chains/base.rb +21 -0
- data/lib/viem/chains/definitions.rb +185 -0
- data/lib/viem/clients/public_client.rb +39 -0
- data/lib/viem/clients/test_client.rb +55 -0
- data/lib/viem/clients/wallet_client.rb +25 -0
- data/lib/viem/errors.rb +63 -0
- data/lib/viem/transports/base.rb +26 -0
- data/lib/viem/transports/fallback.rb +24 -0
- data/lib/viem/transports/http.rb +45 -0
- data/lib/viem/transports/web_socket.rb +64 -0
- data/lib/viem/utils/address.rb +31 -0
- data/lib/viem/utils/hash.rb +27 -0
- data/lib/viem/utils/hex.rb +66 -0
- data/lib/viem/utils/units.rb +37 -0
- data/lib/viem/version.rb +5 -0
- data/lib/viem_rb.rb +71 -0
- 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
|
data/lib/viem/errors.rb
ADDED
|
@@ -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
|