near 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf5d4ecbf47a20dcb7316560abfdf3317505824ed2a501dc769e53186db46571
4
- data.tar.gz: 5745b4bd4742054e9c5e710b4e267795636fd4d82c0866a3209ed804c306b25a
3
+ metadata.gz: 1d113e40c4ca1414cd8bff319acdaeba7d7cd1a07ebc866ce875afd2080e44b1
4
+ data.tar.gz: ae7151a4a1022ecf3b9d652ae11bcabfffc427ccaad0cc07858fd662fb91cbd8
5
5
  SHA512:
6
- metadata.gz: b877435cf142fab1a9d2f4b6651f1993dbf05b7c8eeea28cc76ea9ac6930a855a6570576ae5fb8012e869949efd1b56ca6cce5474bd318735996c9b8713d74f9
7
- data.tar.gz: a0e57d4a298b6e52d320a9c2be9ccc8aef0f6abe7ed24e18535c6dbc0fffdbe153f3b8fa3c676cc33ac5823824d1a878175778684c88d1e272354d841ba9a388
6
+ metadata.gz: 779325138cf044d38991e1911d8bf115f8559d86a530bda11e0b97a75367406386b63e81a7e9a3d9db69b70ab826ce146e0b326efdb9e69dcf3545083227c1bb
7
+ data.tar.gz: f5b55100bc19a9c8d3e6c165e66d7780b94a60ed1fea90bce0523cf77657b384b13c1439c8aabbc4db2b6d2ae5534ef186ad9d7ed82dd2bb3e85ee915f5e1fb0
data/CHANGES.md CHANGED
@@ -5,21 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.3.1 - 2025-02-05
9
+
10
+ ## 0.3.0 - 2025-02-02
11
+
12
+ ### Added
13
+ - Implement block data accessors
14
+ - Implement transaction data accessors
15
+ - Implement action parsing
16
+
17
+ ### Changed
18
+ - Adopt Faraday as the HTTP client
19
+ - Improve HTTP error handling and retries
20
+
8
21
  ## 0.2.0 - 2025-01-29
9
22
 
10
23
  ### Added
11
- - `Network#fetch(block)`.
12
- - `NEAR.testnet`, `NEAR.mainnet`.
24
+ - `Network#fetch(block)`
25
+ - `NEAR.testnet`, `NEAR.mainnet`
13
26
 
14
27
  ### Changed
15
- - Use the NEAR_CLI environmnet variable, if set.
16
- - Use a temporary file for `call_function`.
17
- - Enhance `call_function()`.
18
- - Enhance `create_account_with_funding()`.
19
- - Enhance `delete_account()`.
28
+ - Use the NEAR_CLI environmnet variable, if set
29
+ - Use a temporary file for `call_function`
30
+ - Enhance `call_function()`
31
+ - Enhance `create_account_with_funding()`
32
+ - Enhance `delete_account()`
20
33
 
21
34
  ### Fixed
22
- - Fix the formatting of balances.
35
+ - Fix the formatting of balances
23
36
 
24
37
  ## 0.1.0 - 2025-01-19
25
38
 
data/README.md CHANGED
@@ -6,10 +6,13 @@
6
6
  [![Documentation](https://img.shields.io/badge/rubydoc-latest-blue)](https://rubydoc.info/gems/near)
7
7
 
8
8
  **NEAR.rb** is a [Ruby] client library for the [NEAR Protocol].
9
- It wraps the [NEAR command-line interface] (CLI) into a Ruby interface.
9
+ It provides a [neardata.xyz] API client for block data as well as wraps the
10
+ [NEAR Command-Line Interface] (CLI) in a high-productivity Ruby interface.
10
11
 
11
12
  ## ✨ Features
12
13
 
14
+ - Fetches block data from the [neardata.xyz] API.
15
+ - Supports parsing of block, chunk, transaction, and action data.
13
16
  - Wraps the complete CLI features in an idiomatic Ruby interface.
14
17
  - Provides comprehensive account management operations.
15
18
  - Supports token operations for NEAR and other assets.
@@ -17,14 +20,13 @@ It wraps the [NEAR command-line interface] (CLI) into a Ruby interface.
17
20
  - Handles transaction construction, signing, and monitoring.
18
21
  - Integrates with hardware wallets and secure key storage.
19
22
  - Implements type-safe balance operations and input validation.
20
- - Fetches block data from the [neardata.xyz] API.
21
23
  - Supports both the [mainnet] and [testnet] environments.
22
24
  - Offers cross-platform support with zero library dependencies.
23
25
  - 100% free and unencumbered public domain software.
24
26
 
25
27
  ## 🛠️ Prerequisites
26
28
 
27
- - [NEAR CLI] 0.19+
29
+ - [NEAR CLI] 0.18+
28
30
  - [Ruby] 3.0+
29
31
 
30
32
  ## ⬇️ Installation
@@ -37,6 +39,23 @@ gem install near
37
39
 
38
40
  ## 👉 Examples
39
41
 
42
+ - [Importing the library](#importing-the-library)
43
+ - [Fetching block information](#fetching-block-information)
44
+ - [Tracking block production](#tracking-block-production)
45
+ - [Tracking chain transactions](#tracking-chain-transactions)
46
+ - [Tracking chain actions](#tracking-chain-actions)
47
+ - [Tracking contract interactions](#tracking-contract-interactions)
48
+ - [Instantiating the CLI wrapper](#instantiating-the-cli-wrapper)
49
+ - [Viewing account details](#viewing-account-details)
50
+ - [Importing accounts](#importing-accounts)
51
+ - [Creating accounts](#creating-accounts)
52
+ - [Deleting accounts](#deleting-accounts)
53
+ - [Managing access keys](#managing-access-keys)
54
+ - [Managing tokens](#managing-tokens)
55
+ - [Calling read-only contract methods](#calling-read-only-contract-methods)
56
+ - [Calling mutative contract methods](#calling-mutative-contract-methods)
57
+ - [Deploying a smart contract](#deploying-a-smart-contract)
58
+
40
59
  ### Importing the library
41
60
 
42
61
  ```ruby
@@ -53,6 +72,68 @@ global edge locations:
53
72
  block = NEAR.testnet.fetch(186_132_854)
54
73
  ```
55
74
 
75
+ ### Tracking block production
76
+
77
+ Thanks to the built-in [neardata.xyz] API client, it is trivial to
78
+ track the current tip of the chain in a robust and efficient manner:
79
+
80
+ ```ruby
81
+ NEAR.testnet.fetch_blocks do |block|
82
+ puts [block.height, block.hash].join("\t")
83
+ end
84
+ ```
85
+
86
+ ### Tracking chain transactions
87
+
88
+ ```ruby
89
+ NEAR.testnet.fetch_blocks do |block|
90
+ puts block.inspect
91
+
92
+ block.each_transaction do |transaction|
93
+ puts "\t" + transaction.inspect
94
+ end
95
+ end
96
+ ```
97
+
98
+ For a more elaborated example, see
99
+ [`examples/monitor_all_transactions.rb`](examples/monitor_all_transactions.rb):
100
+
101
+ [![monitor_all_transactions.rb](examples/monitor_all_transactions.gif)](examples/monitor_all_transactions.gif)
102
+
103
+ ### Tracking chain actions
104
+
105
+ ```ruby
106
+ NEAR.testnet.fetch_blocks do |block|
107
+ puts block.inspect
108
+
109
+ block.each_action do |action|
110
+ puts "\t" + action.inspect
111
+ end
112
+ end
113
+ ```
114
+
115
+ For a more elaborated example, see
116
+ [`examples/monitor_all_actions.rb`](examples/monitor_all_actions.rb):
117
+
118
+ [![monitor_all_actions.rb](examples/monitor_all_actions.gif)](examples/monitor_all_actions.gif)
119
+
120
+ ### Tracking contract interactions
121
+
122
+ ```ruby
123
+ NEAR.testnet.fetch_blocks do |block|
124
+ puts block.inspect
125
+
126
+ block.find_actions(:FunctionCall, receiver: 'aurora', method_name: /^submit/) do |action|
127
+ puts "\t" + action.inspect
128
+ end
129
+ end
130
+ ```
131
+
132
+ For a more elaborated example, see
133
+ [`examples/index_evm_transactions.rb`](examples/index_evm_transactions.rb):
134
+
135
+ [![index_evm_transactions.rb](examples/index_evm_transactions.gif)](examples/index_evm_transactions.gif)
136
+
56
137
  ### Instantiating the CLI wrapper
57
138
 
58
139
  ```ruby
@@ -106,7 +187,7 @@ result = testnet.import_account_with_private_key(
106
187
  # Create an account funded from a faucet (testnet only):
107
188
  result = testnet.create_account_with_faucet(
108
189
  'mynewaccount.testnet',
109
- 'ed25519:HVPgAsZkZ7cwLZDqK313XJsDyqAvgBxrATcD7VacA8KE'
190
+ public_key: 'ed25519:HVPgAsZkZ7cwLZDqK313XJsDyqAvgBxrATcD7VacA8KE'
110
191
  )
111
192
 
112
193
  # Create an account funded by another account:
@@ -126,8 +207,8 @@ result = testnet.create_implicit_account('/path/to/credentials/folder')
126
207
  ```ruby
127
208
  # Delete an existing account:
128
209
  result = testnet.delete_account(
129
- 'todelete.testnet', # account to delete
130
- 'beneficiary.testnet' # account receiving remaining balance
210
+ 'my-obsolete-account.testnet', # account to delete
211
+ beneficiary: 'v2.faucet.nonofficial.testnet' # account receiving remaining balance
131
212
  )
132
213
  ```
133
214
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.1
data/lib/near/account.rb CHANGED
@@ -5,10 +5,20 @@
5
5
  #
6
6
  # @see https://nomicon.io/DataStructures/Account
7
7
  class NEAR::Account
8
+ ##
9
+ # @param [String, #to_s] id
10
+ # @return [NEAR::Account]
8
11
  def self.parse(id)
9
12
  self.new(id.to_s)
10
13
  end
11
14
 
15
+ ##
16
+ # @return [NEAR::Account]
17
+ def self.temp
18
+ timestamp = (Time.now.to_f * 1_000).to_i
19
+ self.new("temp-#{timestamp}.testnet")
20
+ end
21
+
12
22
  ##
13
23
  # @param [String, #to_s] id
14
24
  def initialize(id)
@@ -0,0 +1,141 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require 'base64'
4
+ require 'json'
5
+
6
+ ##
7
+ # Represents a NEAR action.
8
+ #
9
+ # @see https://nomicon.io/RuntimeSpec/Actions
10
+ class NEAR::Action
11
+ class CreateAccount < NEAR::Action; end
12
+
13
+ class DeployContract < NEAR::Action; end
14
+
15
+ class FunctionCall < NEAR::Action
16
+ ##
17
+ # @return [String]
18
+ attr_reader :method_name
19
+ def method_name
20
+ @data['method_name']
21
+ end
22
+
23
+ ##
24
+ # @return [String]
25
+ def json_args
26
+ @json_args ||= JSON.parse(self.args)
27
+ end
28
+
29
+ ##
30
+ # @param [Symbol, String, #to_s] key
31
+ # @return [Object]
32
+ def [](key)
33
+ self.json_args[key.to_s] rescue nil
34
+ end
35
+
36
+ ##
37
+ # @return [String]
38
+ attr_reader :args
39
+ def args
40
+ @args ||= Base64.decode64(@data['args'])
41
+ end
42
+
43
+ ##
44
+ # @return [Integer]
45
+ attr_reader :gas
46
+ def gas
47
+ @data['gas']
48
+ end
49
+
50
+ ##
51
+ # @return [NEAR::Balance]
52
+ attr_reader :deposit
53
+ def deposit
54
+ @deposit ||= NEAR::Balance.parse(@data['deposit'])
55
+ end
56
+ end
57
+
58
+ class Transfer < NEAR::Action
59
+ ##
60
+ # @return [NEAR::Balance]
61
+ attr_reader :deposit
62
+ def deposit
63
+ @deposit ||= NEAR::Balance.parse(@data['deposit'])
64
+ end
65
+ end
66
+
67
+ class Stake < NEAR::Action; end
68
+
69
+ class AddKey < NEAR::Action; end
70
+
71
+ class DeleteKey < NEAR::Action; end
72
+
73
+ class DeleteAccount < NEAR::Action; end
74
+
75
+ class Delegate < NEAR::Action; end
76
+
77
+ ##
78
+ # @param [String, Hash] data
79
+ # @return [NEAR::Action]
80
+ def self.parse(data)
81
+ case
82
+ when data.is_a?(String) then case
83
+ when 'CreateAccount' then CreateAccount.new
84
+ when 'DeployContract' then DeployContract.new
85
+ when 'FunctionCall' then FunctionCall.new
86
+ when 'Transfer' then Transfer.new
87
+ when 'Stake' then Stake.new
88
+ when 'AddKey' then AddKey.new
89
+ when 'DeleteKey' then DeleteKey.new
90
+ when 'DeleteAccount' then DeleteAccount.new
91
+ when 'Delegate' then Delegate.new
92
+ else self.new
93
+ end
94
+ when data['CreateAccount'] then CreateAccount.new(data['CreateAccount'])
95
+ when data['DeployContract'] then DeployContract.new(data['DeployContract'])
96
+ when data['FunctionCall'] then FunctionCall.new(data['FunctionCall'])
97
+ when data['Transfer'] then Transfer.new(data['Transfer'])
98
+ when data['Stake'] then Stake.new(data['Stake'])
99
+ when data['AddKey'] then AddKey.new(data['AddKey'])
100
+ when data['DeleteKey'] then DeleteKey.new(data['DeleteKey'])
101
+ when data['DeleteAccount'] then DeleteAccount.new(data['DeleteAccount'])
102
+ when data['Delegate'] then Delegate.new(data['Delegate'])
103
+ else self.new(data[data.keys.first])
104
+ end
105
+ end
106
+
107
+ ##
108
+ # @param [Hash, #to_h] data
109
+ def initialize(data = nil)
110
+ @data = data.to_h if data
111
+ end
112
+
113
+ ##
114
+ # @return [Symbol]
115
+ attr_reader :type
116
+ def type
117
+ self.class.name.split('::').last.to_sym
118
+ end
119
+
120
+ ##
121
+ # The action data.
122
+ #
123
+ # @return [Hash]
124
+ attr_reader :data
125
+
126
+ ##
127
+ # @return [Hash]
128
+ def to_h; @data || {}; end
129
+
130
+ ##
131
+ # @return [Symbol]
132
+ def to_sym; self.type; end
133
+
134
+ ##
135
+ # @return [String]
136
+ def inspect
137
+ data = self.to_h
138
+ data['args'] = '...' if data['args']
139
+ "#<#{self.class.name} #{data.to_json}>"
140
+ end
141
+ end # NEAR::Block
data/lib/near/balance.rb CHANGED
@@ -5,6 +5,10 @@ require 'bigdecimal'
5
5
  ##
6
6
  # Represents a NEAR balance.
7
7
  class NEAR::Balance
8
+ def self.parse(s)
9
+ self.new(s.to_f / 10**24)
10
+ end
11
+
8
12
  def self.from_near(s)
9
13
  self.new(s)
10
14
  end
@@ -32,7 +36,7 @@ class NEAR::Balance
32
36
  #
33
37
  # @return [String]
34
38
  def inspect
35
- "Ⓝ#{@quantity.to_f}"
39
+ "Ⓝ #{@quantity.to_f}"
36
40
  end
37
41
 
38
42
  ##
data/lib/near/block.rb CHANGED
@@ -26,9 +26,11 @@ class NEAR::Block
26
26
  ##
27
27
  # @param [Integer, #to_i] height
28
28
  # @param [String, #to_s] hash
29
- def initialize(height: nil, hash: nil)
29
+ # @param [Hash, #to_h] data
30
+ def initialize(height: nil, hash: nil, data: nil)
30
31
  @height = height.to_i if height
31
32
  @hash = hash.to_s if hash
33
+ @data = data.to_h if data
32
34
  end
33
35
 
34
36
  ##
@@ -43,6 +45,133 @@ class NEAR::Block
43
45
  # @return [String]
44
46
  attr_reader :hash
45
47
 
48
+ ##
49
+ # The block data, if fetched.
50
+ #
51
+ # @return [Hash]
52
+ attr_reader :data
53
+
54
+ ##
55
+ # @return [String]
56
+ attr_reader :author
57
+ def author
58
+ self.data['block']['author']
59
+ end
60
+
61
+ ##
62
+ # The block header.
63
+ #
64
+ # @return [Hash]
65
+ attr_reader :header
66
+ def header
67
+ self.data['block']['header']
68
+ end
69
+
70
+ ##
71
+ # @return [Array<Hash>]
72
+ attr_reader :chunks
73
+ def chunks
74
+ self.data['block']['chunks']
75
+ end
76
+
77
+ ##
78
+ # @return [Array<Hash>]
79
+ attr_reader :shards
80
+ def shards
81
+ self.data['shards']
82
+ end
83
+
84
+ ##
85
+ # The set of signers in the transactions in this block.
86
+ #
87
+ # @return [Array<NEAR::Account>]
88
+ def signers
89
+ self.collect_transaction_field('signer_id')
90
+ .map { |id| NEAR::Account.new(id) }
91
+ end
92
+
93
+ ##
94
+ # The set of receivers in the transactions in this block.
95
+ #
96
+ # @return [Array<NEAR::Account>]
97
+ def receivers
98
+ self.collect_transaction_field('receiver_id')
99
+ .map { |id| NEAR::Account.new(id) }
100
+ end
101
+
102
+ ##
103
+ # Enumerates the transactions in this block.
104
+ #
105
+ # @yield [NEAR::Transaction]
106
+ # @yieldparam [NEAR::Transaction] transaction
107
+ # @yieldreturn [void]
108
+ # @return [Enumerator] if no block is given
109
+ def each_transaction(&)
110
+ return enum_for(:each_transaction) unless block_given?
111
+ self.each_chunk do |chunk|
112
+ next if !chunk['transactions']
113
+ chunk['transactions'].each do |tx|
114
+ yield NEAR::Transaction.parse(tx)
115
+ end
116
+ end
117
+ end
118
+
119
+ ##
120
+ # Enumerates the actions in this block.
121
+ #
122
+ # @yield [NEAR::Action, NEAR::Transaction]
123
+ # @yieldparam [NEAR::Action] action
124
+ # @yieldparam [NEAR::Transaction] transaction
125
+ # @yieldreturn [void]
126
+ # @return [Enumerator] if no block is given
127
+ def each_action(&)
128
+ return enum_for(:each_action) unless block_given?
129
+ self.each_transaction do |tx|
130
+ tx.each_action do |action|
131
+ yield action, tx
132
+ end
133
+ end
134
+ end
135
+
136
+ ##
137
+ # @param [Symbol, #to_sym] type
138
+ # @param [String, Regexp] signer
139
+ # @param [String, Regexp] receiver
140
+ # @param [String, Regexp] method_name
141
+ # @yield [NEAR::Action, NEAR::Transaction]
142
+ # @yieldparam [NEAR::Action] action
143
+ # @yieldparam [NEAR::Transaction] transaction
144
+ # @yieldreturn [void]
145
+ # @return [Enumerator] if no block is given
146
+ def find_actions(type, signer: nil, receiver: nil, method_name: nil, &)
147
+ return enum_for(:each_action) unless block_given?
148
+ type = type.to_sym
149
+ self.each_transaction do |tx|
150
+ next if signer && !(signer === tx.signer_id)
151
+ next if receiver && !(receiver === tx.receiver_id)
152
+ tx.each_action do |action|
153
+ next unless type == action.type
154
+ next if method_name && !(method_name === action.method_name)
155
+ yield action, tx
156
+ end
157
+ end
158
+ end
159
+
160
+ ##
161
+ # Enumerates the chunks in this block.
162
+ #
163
+ # @yield [Hash]
164
+ # @yieldparam [Hash] chunk
165
+ # @yieldreturn [void]
166
+ # @return [Enumerator] if no block is given
167
+ def each_chunk(&)
168
+ return enum_for(:each_chunk) unless block_given?
169
+ self.shards.each do |shard|
170
+ chunk = shard['chunk']
171
+ yield chunk if chunk
172
+ end
173
+ end
174
+
46
175
  ##
47
176
  # @return [Array<String>]
48
177
  def to_cli_args
@@ -53,13 +182,36 @@ class NEAR::Block
53
182
 
54
183
  ##
55
184
  # @return [Integer]
56
- def to_i
57
- @height
58
- end
185
+ def to_i; @height; end
59
186
 
60
187
  ##
61
188
  # @return [String]
62
189
  def to_s
63
190
  (@height || @hash || :now).to_s
64
191
  end
192
+
193
+ ##
194
+ # @return [Hash]
195
+ def to_h; @data; end
196
+
197
+ ##
198
+ # @return [String]
199
+ def inspect
200
+ "#<#{self.class.name} height: #{@height}, hash: #{@hash.inspect}>"
201
+ end
202
+
203
+ protected
204
+
205
+ ##
206
+ # @param [String] field
207
+ # @return [Array<String>]
208
+ def collect_transaction_field(field)
209
+ result = {}
210
+ self.shards.each do |shard|
211
+ shard['chunk']['transactions'].each do |tx|
212
+ result[tx['transaction']['receiver_id']] = true
213
+ end
214
+ end
215
+ result.keys
216
+ end
65
217
  end # NEAR::Block
@@ -70,15 +70,20 @@ module NEAR::CLI::Account
70
70
  ##
71
71
  # Creates a new account sponsored by the faucet service.
72
72
  #
73
- # @param [String] new_account_id
74
- # @param [String] public_key
73
+ # @param [NEAR::Account, #to_s] new_account
74
+ # @param [String, nil] public_key
75
75
  # @return [String]
76
- def create_account_with_faucet(new_account_id, public_key)
76
+ def create_account_with_faucet(new_account, public_key: nil)
77
77
  stdout, stderr = execute(
78
78
  'account',
79
79
  'create-account',
80
- 'sponsor-by-faucet-service', new_account_id,
81
- 'use-manually-provided-public-key', public_key,
80
+ 'sponsor-by-faucet-service', new_account.to_s,
81
+ *case public_key
82
+ when nil then ['autogenerate-new-keypair', 'save-to-keychain']
83
+ when String then ['use-manually-provided-public-key', public_key]
84
+ when Array then public_key
85
+ else raise ArgumentError
86
+ end,
82
87
  'network-config', @network,
83
88
  'create'
84
89
  )
@@ -88,16 +93,22 @@ module NEAR::CLI::Account
88
93
  ##
89
94
  # Creates a new account funded by another account.
90
95
  #
91
- # @param [NEAR::Account] new_account
92
- # @param [NEAR::Account] signer Account that signs & funds the transaction
93
- # @param [NEAR::Balance] deposit Amount of NEAR to attach
96
+ # @param [NEAR::Account, #to_s] new_account
97
+ # @param [NEAR::Account, #to_s] signer Account that signs & funds the transaction
98
+ # @param [String, nil] public_key
99
+ # @param [NEAR::Balance, #to_s] deposit Amount of NEAR to attach
94
100
  # @return [String]
95
- def create_account_with_funding(new_account, signer:, deposit: nil)
101
+ def create_account_with_funding(new_account, signer:, public_key: nil, deposit: nil)
96
102
  stdout, stderr = execute(
97
103
  'account',
98
104
  'create-account',
99
105
  'fund-myself', new_account.to_s, (deposit ? deposit.to_s : '0') + ' NEAR',
100
- 'autogenerate-new-keypair', 'save-to-keychain',
106
+ *case public_key
107
+ when nil then ['autogenerate-new-keypair', 'save-to-keychain']
108
+ when String then ['use-manually-provided-public-key', public_key]
109
+ when Array then public_key
110
+ else raise ArgumentError
111
+ end,
101
112
  'sign-as', signer.to_s,
102
113
  'network-config', @network,
103
114
  'sign-with-keychain',
data/lib/near/error.rb ADDED
@@ -0,0 +1,18 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ class NEAR::Error < StandardError
4
+ class InvalidBlock < NEAR::Error
5
+ def initialize(block_height = nil)
6
+ @block_height = block_height.to_i if block_height
7
+ super(block_height ?
8
+ "Block height ##{block_height.to_i} does not exist" :
9
+ "Block height does not exist")
10
+ end
11
+
12
+ attr_reader :block_height
13
+ end # NEAR::Error::InvalidBlock
14
+
15
+ class TemporaryProblem < NEAR::Error; end
16
+
17
+ class UnexpectedProblem < NEAR::Error; end
18
+ end # NEAR::Error
data/lib/near/network.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # This is free and unencumbered software released into the public domain.
2
2
 
3
- require 'net/http'
3
+ require 'faraday'
4
+ require 'faraday/follow_redirects'
5
+ require 'faraday/retry'
6
+
7
+ require_relative 'error'
4
8
 
5
9
  ##
6
10
  # Represents a NEAR Protocol network.
@@ -24,20 +28,107 @@ class NEAR::Network
24
28
  self.class.const_get(:NEARDATA_URL)
25
29
  end
26
30
 
31
+ ##
32
+ # Fetches a block range.
33
+ #
34
+ # The block data is fetched from the neardata.xyz API.
35
+ #
36
+ # @param [NEAR::Block, #to_i] from
37
+ # @param [NEAR::Block, #to_i] to
38
+ # @yield [NEAR::Block]
39
+ # @yieldparam [NEAR::Block] block
40
+ # @yieldreturn [void]
41
+ # @return [Enumerator]
42
+ def fetch_blocks(from: nil, to: nil, &)
43
+ return enum_for(:fetch_blocks) unless block_given?
44
+ from = self.fetch_latest.height unless from
45
+ from, to = from.to_i, to&.to_i
46
+ (from..to).each do |block_height|
47
+ yield self.fetch(block_height)
48
+ end
49
+ end
50
+
27
51
  ##
28
52
  # Fetches the block at the given height.
29
53
  #
30
54
  # The block data is fetched from the neardata.xyz API.
31
- # If the block does not exist, `nil` is returned.
32
55
  #
33
56
  # @param [NEAR::Block, #to_i] block
34
- # @return [Object]
57
+ # @return [NEAR::Block]
58
+ # @raise [NEAR::Error::InvalidBlock] if the block does not exist
35
59
  def fetch(block)
36
- response = Net::HTTP.get_response(URI("#{neardata_url}/v0/block/#{block.to_i}"))
37
- case response
38
- when Net::HTTPSuccess then JSON.parse(response.body)
39
- when Net::HTTPNotFound then nil
40
- else raise response.to_s
60
+ self.fetch_neardata_block(block.to_i)
61
+ end
62
+
63
+ ##
64
+ # Fetches the latest finalized block.
65
+ #
66
+ # The block data is fetched from the neardata.xyz API.
67
+ # The block is guaranteed to exist.
68
+ #
69
+ # @return [NEAR::Block]
70
+ def fetch_latest
71
+ self.fetch_neardata_block(:final)
72
+ end
73
+
74
+ protected
75
+
76
+ ##
77
+ # @param [Integer, Symbol] block_id
78
+ # @return [NEAR::Block]
79
+ def fetch_neardata_block(block_id)
80
+ request_path = case block_id
81
+ when Integer then "/v0/block/#{block_id}"
82
+ when Symbol then "/v0/last_block/#{block_id}"
83
+ else raise ArgumentError
84
+ end
85
+
86
+ begin
87
+ block_data = self.fetch_neardata_url(request_path)
88
+ rescue Faraday::ResourceNotFound => error
89
+ case error.response[:body]['type']
90
+ when "BLOCK_DOES_NOT_EXIST", "BLOCK_HEIGHT_TOO_LOW", "BLOCK_HEIGHT_TOO_HIGH"
91
+ raise NEAR::Error::InvalidBlock, block_id
92
+ else raise NEAR::Error::UnexpectedProblem
93
+ end
94
+ end
95
+
96
+ return nil if block_data.nil? || block_data == 'null'
97
+ block_header = block_data['block']['header']
98
+ block_height = block_header['height'].to_i
99
+ block_hash = block_header['hash'].to_s
100
+ NEAR::Block.new(height: block_height, hash: block_hash, data: block_data)
101
+ end
102
+
103
+ ##
104
+ # @return [Object]
105
+ def fetch_neardata_url(path)
106
+ begin
107
+ retries ||= 0
108
+ self.http_client.get("#{neardata_url}#{path}").body
109
+ rescue Faraday::ConnectionFailed => error
110
+ # `Faraday::Retry` doesn't seem able to handle `ConnectionFailed`,
111
+ # so we'll handle it manually here. Note that in case of a DNS error,
112
+ # there's no point in retrying, so we'll wrap & re-raise the error.
113
+ case error.wrapped_exception
114
+ when Socket::ResolutionError then raise NEAR::Error::TemporaryProblem
115
+ else retry if (retries += 1) < 3
116
+ end
117
+ raise NEAR::Error::UnexpectedProblem
118
+ end
119
+ end
120
+
121
+ ##
122
+ # @return [Faraday::Connection]
123
+ def http_client
124
+ Faraday.new do |faraday|
125
+ faraday.response :raise_error
126
+ faraday.response :follow_redirects
127
+ faraday.response :json
128
+ faraday.request :retry, {
129
+ max: 3,
130
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed]
131
+ }.freeze
41
132
  end
42
133
  end
43
134
  end # NEAR::Testnet
@@ -3,10 +3,21 @@
3
3
  ##
4
4
  # Represents a NEAR transaction.
5
5
  class NEAR::Transaction
6
+ ##
7
+ # @param [Hash] json
8
+ # @return [NEAR::Transaction]
9
+ def self.parse(json)
10
+ tx_data = json['transaction']
11
+ NEAR::Transaction.new(tx_data['hash'], data: tx_data)
12
+ end
13
+
6
14
  ##
7
15
  # @param [String, #to_s] hash
8
- def initialize(hash)
16
+ # @param [Hash, #to_h] data
17
+ # @return [void]
18
+ def initialize(hash, data: nil)
9
19
  @hash = hash.to_s
20
+ @data = data.to_h if data
10
21
  end
11
22
 
12
23
  ##
@@ -15,7 +26,108 @@ class NEAR::Transaction
15
26
  # @return [String]
16
27
  attr_reader :hash
17
28
 
29
+ ##
30
+ # The transaction data, if available.
31
+ #
32
+ # @return [Hash]
33
+ attr_reader :data
34
+
35
+ ##
36
+ # The transaction signer account.
37
+ #
38
+ # @return [NEAR::Account]
39
+ attr_reader :signer
40
+ def signer
41
+ @signer ||= NEAR::Account.new(self.signer_id)
42
+ end
43
+
44
+ ##
45
+ # The transaction signer ID.
46
+ #
47
+ # @return [String]
48
+ attr_reader :signer_id
49
+ def signer_id
50
+ self.data['signer_id']
51
+ end
52
+
53
+ ##
54
+ # The transaction receiver account.
55
+ #
56
+ # @return [NEAR::Account]
57
+ attr_reader :receiver
58
+ def receiver
59
+ @receiver ||= NEAR::Account.new(self.receiver_id)
60
+ end
61
+
62
+ ##
63
+ # The transaction receiver ID.
64
+ #
65
+ # @return [String]
66
+ attr_reader :receiver_id
67
+ def receiver_id
68
+ self.data['receiver_id']
69
+ end
70
+
71
+ ##
72
+ # @return [String]
73
+ attr_reader :public_key
74
+ def public_key
75
+ self.data['public_key']
76
+ end
77
+
78
+ ##
79
+ # @return [String]
80
+ attr_reader :signature
81
+ def signature
82
+ self.data['signature']
83
+ end
84
+
85
+ ##
86
+ # @return [Integer]
87
+ attr_reader :nonce
88
+ def nonce
89
+ self.data['nonce']
90
+ end
91
+
92
+ ##
93
+ # @return [Integer]
94
+ attr_reader :priority_fee
95
+ def priority_fee
96
+ self.data['priority_fee']
97
+ end
98
+
99
+ ##
100
+ # The transaction actions.
101
+ #
102
+ # @return [Array<Hash>]
103
+ attr_reader :actions
104
+ def actions
105
+ self.data['actions']
106
+ end
107
+
108
+ ##
109
+ # @yield [NEAR::Action]
110
+ # @yieldparam [NEAR::Action] action
111
+ # @yieldreturn [void]
112
+ # @return [Enumerator] if no block is given
113
+ def each_action(&)
114
+ return enum_for(:each_action) unless block_given?
115
+ self.data['actions'].each do |action|
116
+ yield NEAR::Action.parse(action)
117
+ end
118
+ end
119
+
18
120
  ##
19
121
  # @return [String]
20
122
  def to_s; @hash; end
123
+
124
+ ##
125
+ # @return [Hash]
126
+ def to_h; @data; end
127
+
128
+ ##
129
+ # @return [String]
130
+ def inspect
131
+ "#<#{self.class.name} hash: #{@hash.inspect}, signer: #{self.signer}, receiver: #{self.receiver}, actions: #{self.actions.size}>"
132
+ end
21
133
  end # NEAR::Transaction
data/lib/near.rb CHANGED
@@ -3,9 +3,11 @@
3
3
  module NEAR; end
4
4
 
5
5
  require_relative 'near/account'
6
+ require_relative 'near/action'
6
7
  require_relative 'near/balance'
7
8
  require_relative 'near/block'
8
9
  require_relative 'near/cli'
10
+ require_relative 'near/error'
9
11
  require_relative 'near/mainnet'
10
12
  require_relative 'near/network'
11
13
  require_relative 'near/testnet'
metadata CHANGED
@@ -1,14 +1,84 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: near
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arto Bendiken
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-29 00:00:00.000000000 Z
10
+ date: 2025-02-05 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: bigdecimal
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: faraday
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.12'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '2.12'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday-follow_redirects
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0.3'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday-retry
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
12
82
  - !ruby/object:Gem::Dependency
13
83
  name: rspec
14
84
  requirement: !ruby/object:Gem::Requirement
@@ -50,6 +120,7 @@ files:
50
120
  - VERSION
51
121
  - lib/near.rb
52
122
  - lib/near/account.rb
123
+ - lib/near/action.rb
53
124
  - lib/near/balance.rb
54
125
  - lib/near/block.rb
55
126
  - lib/near/cli.rb
@@ -59,6 +130,7 @@ files:
59
130
  - lib/near/cli/staking.rb
60
131
  - lib/near/cli/tokens.rb
61
132
  - lib/near/cli/transaction.rb
133
+ - lib/near/error.rb
62
134
  - lib/near/mainnet.rb
63
135
  - lib/near/network.rb
64
136
  - lib/near/testnet.rb
@@ -87,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
159
  - !ruby/object:Gem::Version
88
160
  version: '0'
89
161
  requirements: []
90
- rubygems_version: 3.6.2
162
+ rubygems_version: 3.6.3
91
163
  specification_version: 4
92
164
  summary: 'NEAR.rb: NEAR for Ruby'
93
165
  test_files: []