near 0.2.0 → 0.3.1

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 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: []