near 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ab3d2b8796c8b9d6db6d842a843882a02b1497070454e67dda41a2105dbe7aa
4
- data.tar.gz: 66de38abb9b8bd7162116f110ae2a3f81412f7e580bb2e479db5133d2504f725
3
+ metadata.gz: 147f21ee313f56cb2e1327e8817df79796dd56fe90eff771e9057e6d7248c4f5
4
+ data.tar.gz: bf81f3240cb7e18c519d85696bf472b1bf2c8e56c3049aa942fe2aaae37e38f6
5
5
  SHA512:
6
- metadata.gz: 8cead3d3f26398a34925d6ba326c981f19be8e4324348f7dd8879a1c291ad11bd80080c11023477e3f4ee5bbfcf5bddfc421e85bea05f8e42acfd672e28bea0f
7
- data.tar.gz: f11b1a7a3c795865375feb3c873e9158009b6508a06150d91b3dff126b074920b2c91e658c6bc121e91098f64c198e47aaff0a7ee1348adb1e39364212eb2db5
6
+ metadata.gz: 4cfef8be7777c9dd2f2f839284581a44a0f2479f256f09e759628c21509a6d3bc9b95ad07c7e0e13a2e8c22aa5238bd826a224cdac9c6e649388bd060971569d
7
+ data.tar.gz: 4c5e69608fe79e7a1e0f7a9046477ea9c590d61d1ee6bf12b530054775fc26464d68ddd29c6c9655f05459faf2752a4c7119e5a29afb17a2500ba115618d0b3d
data/CHANGES.md CHANGED
@@ -5,6 +5,33 @@ 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.0 - 2025-02-02
9
+
10
+ ### Added
11
+ - Implement block data accessors
12
+ - Implement transaction data accessors
13
+ - Implement action parsing
14
+
15
+ ### Changed
16
+ - Adopt Faraday as the HTTP client
17
+ - Improve HTTP error handling and retries
18
+
19
+ ## 0.2.0 - 2025-01-29
20
+
21
+ ### Added
22
+ - `Network#fetch(block)`
23
+ - `NEAR.testnet`, `NEAR.mainnet`
24
+
25
+ ### Changed
26
+ - Use the NEAR_CLI environmnet variable, if set
27
+ - Use a temporary file for `call_function`
28
+ - Enhance `call_function()`
29
+ - Enhance `create_account_with_funding()`
30
+ - Enhance `delete_account()`
31
+
32
+ ### Fixed
33
+ - Fix the formatting of balances
34
+
8
35
  ## 0.1.0 - 2025-01-19
9
36
 
10
37
  ## 0.0.0 - 2025-01-11
data/README.md CHANGED
@@ -6,10 +6,12 @@
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 as well as wraps the
10
+ [NEAR command-line interface] (CLI) in a usable Ruby interface.
10
11
 
11
12
  ## ✨ Features
12
13
 
14
+ - Fetches block data from the [neardata.xyz] API.
13
15
  - Wraps the complete CLI features in an idiomatic Ruby interface.
14
16
  - Provides comprehensive account management operations.
15
17
  - Supports token operations for NEAR and other assets.
@@ -23,7 +25,7 @@ It wraps the [NEAR command-line interface] (CLI) into a Ruby interface.
23
25
 
24
26
  ## 🛠️ Prerequisites
25
27
 
26
- - [NEAR CLI] 0.17+
28
+ - [NEAR CLI] 0.19+
27
29
  - [Ruby] 3.0+
28
30
 
29
31
  ## ⬇️ Installation
@@ -36,16 +38,100 @@ gem install near
36
38
 
37
39
  ## 👉 Examples
38
40
 
41
+ - [Importing the library](#importing-the-library)
42
+ - [Fetching block information](#fetching-block-information)
43
+ - [Tracking block production](#tracking-block-production)
44
+ - [Tracking chain transactions](#tracking-chain-transactions)
45
+ - [Tracking chain actions](#tracking-chain-actions)
46
+ - [Tracking contract interactions](#tracking-contract-interactions)
47
+ - [Instantiating the CLI wrapper](#instantiating-the-cli-wrapper)
48
+ - [Viewing account details](#viewing-account-details)
49
+ - [Importing accounts](#importing-accounts)
50
+ - [Creating accounts](#creating-accounts)
51
+ - [Deleting accounts](#deleting-accounts)
52
+ - [Managing access keys](#managing-access-keys)
53
+ - [Managing tokens](#managing-tokens)
54
+ - [Calling read-only contract methods](#calling-read-only-contract-methods)
55
+ - [Calling mutative contract methods](#calling-mutative-contract-methods)
56
+ - [Deploying a smart contract](#deploying-a-smart-contract)
57
+
39
58
  ### Importing the library
40
59
 
41
60
  ```ruby
42
61
  require 'near'
62
+ ```
63
+
64
+ ### Fetching block information
65
+
66
+ Block data is fetched from the high-performance [neardata.xyz] API
67
+ that caches blocks using Cloudflare's network with more than 330
68
+ global edge locations:
69
+
70
+ ```ruby
71
+ block = NEAR.testnet.fetch(186_132_854)
72
+ ```
73
+
74
+ ### Tracking block production
75
+
76
+ Thanks to the built-in [neardata.xyz] API client, it is trivial to
77
+ track the current tip of the chain in a robust and efficient manner:
78
+
79
+ ```ruby
80
+ NEAR.testnet.fetch_blocks do |block|
81
+ puts [block.height, block.hash].join("\t")
82
+ end
83
+ ```
84
+
85
+ ### Tracking chain transactions
86
+
87
+ See [`examples/monitor_all_transactions.rb`](examples/monitor_all_transactions.rb).
88
+
89
+ ```ruby
90
+ NEAR.testnet.fetch_blocks do |block|
91
+ puts block.inspect
43
92
 
93
+ block.each_transaction do |transaction|
94
+ puts "\t" + transaction.inspect
95
+ end
96
+ end
97
+ ```
98
+
99
+ ### Tracking chain actions
100
+
101
+ See [`examples/monitor_all_actions.rb`](examples/monitor_all_actions.rb).
102
+
103
+ ```ruby
104
+ NEAR.testnet.fetch_blocks do |block|
105
+ puts block.inspect
106
+
107
+ block.each_action do |action|
108
+ puts "\t" + action.inspect
109
+ end
110
+ end
111
+ ```
112
+
113
+ ### Tracking contract interactions
114
+
115
+ See [`examples/index_evm_transactions.rb`](examples/index_evm_transactions.rb).
116
+
117
+ ```ruby
118
+ NEAR.testnet.fetch_blocks do |block|
119
+ puts block.inspect
120
+
121
+ block.find_actions(:FunctionCall, receiver: 'aurora', method_name: /^submit/) do |action|
122
+ puts "\t" + action.inspect
123
+ end
124
+ end
125
+ ```
126
+
127
+ ### Instantiating the CLI wrapper
128
+
129
+ ```ruby
44
130
  # Use the NEAR testnet (the default):
45
- testnet = NEAR::CLI.new(network: 'testnet')
131
+ testnet = NEAR::CLI.new(network: NEAR.testnet)
46
132
 
47
133
  # Use the NEAR mainnet (to test in prod):
48
- # mainnet = NEAR::CLI.new(network: 'mainnet')
134
+ # mainnet = NEAR::CLI.new(network: NEAR.mainnet)
49
135
  ```
50
136
 
51
137
  ### Viewing account details
@@ -230,3 +316,4 @@ git clone https://github.com/dryruby/near.rb.git
230
316
  [Ruby]: https://ruby-lang.org
231
317
  [mainnet]: https://docs.near.org/concepts/basics/networks#mainnet
232
318
  [testnet]: https://docs.near.org/concepts/basics/networks#testnet
319
+ [neardata.xyz]: https://neardata.xyz
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.3.0
data/lib/near/account.rb CHANGED
@@ -5,12 +5,23 @@
5
5
  #
6
6
  # @see https://nomicon.io/DataStructures/Account
7
7
  class NEAR::Account
8
+ def self.parse(id)
9
+ self.new(id.to_s)
10
+ end
11
+
8
12
  ##
9
13
  # @param [String, #to_s] id
10
14
  def initialize(id)
11
15
  @id = id.to_s
12
16
  end
13
17
 
18
+ ##
19
+ # @return [NEAR::Account]
20
+ def parent
21
+ return nil unless @id.include?('.')
22
+ self.class.new(@id.split('.').drop(1).join('.'))
23
+ end
24
+
14
25
  ##
15
26
  # The account name.
16
27
  #
@@ -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
@@ -23,19 +27,28 @@ class NEAR::Balance
23
27
  end
24
28
  end
25
29
 
30
+ ##
31
+ # The canonical zero balance.
32
+ ZERO = self.new(0).freeze
33
+
26
34
  ##
27
35
  # The balance as a Ⓝ-prefixed string.
28
36
  #
29
37
  # @return [String]
30
38
  def inspect
31
- "Ⓝ#{@quantity.to_f}"
39
+ "Ⓝ #{@quantity.to_f}"
32
40
  end
33
41
 
34
42
  ##
35
43
  # The balance as a string.
36
44
  #
37
45
  # @return [String]
38
- def to_s; @quantity.to_s; end
46
+ def to_s
47
+ case @quantity
48
+ when BigDecimal then @quantity.to_s('F')
49
+ else @quantity.to_s
50
+ end
51
+ end
39
52
 
40
53
  ##
41
54
  # The balance as a rational number.
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
@@ -51,9 +180,38 @@ class NEAR::Block
51
180
  ['now']
52
181
  end
53
182
 
183
+ ##
184
+ # @return [Integer]
185
+ def to_i; @height; end
186
+
54
187
  ##
55
188
  # @return [String]
56
189
  def to_s
57
190
  (@height || @hash || :now).to_s
58
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
59
217
  end # NEAR::Block
@@ -88,18 +88,17 @@ module NEAR::CLI::Account
88
88
  ##
89
89
  # Creates a new account funded by another account.
90
90
  #
91
- # @param [String] new_account_id
92
- # @param [String] funding_account_id
93
- # @param [String] public_key
94
- # @param [String] deposit
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
95
94
  # @return [String]
96
- def create_account_with_funding(new_account_id, funding_account_id, public_key, deposit)
95
+ def create_account_with_funding(new_account, signer:, deposit: nil)
97
96
  stdout, stderr = execute(
98
97
  'account',
99
98
  'create-account',
100
- 'fund-myself', new_account_id, deposit,
101
- 'use-manually-provided-public-key', public_key,
102
- 'sign-as', funding_account_id,
99
+ 'fund-myself', new_account.to_s, (deposit ? deposit.to_s : '0') + ' NEAR',
100
+ 'autogenerate-new-keypair', 'save-to-keychain',
101
+ 'sign-as', signer.to_s,
103
102
  'network-config', @network,
104
103
  'sign-with-keychain',
105
104
  'send'
@@ -126,14 +125,15 @@ module NEAR::CLI::Account
126
125
  ##
127
126
  # Deletes an account and transfers remaining balance to beneficiary.
128
127
  #
129
- # @param [String] account_id
130
- # @param [String] beneficiary_id
128
+ # @param [NEAR::Account] account
129
+ # @param [NEAR::Account] beneficiary
131
130
  # @return [String]
132
- def delete_account(account_id, beneficiary_id)
131
+ def delete_account(account, beneficiary: nil)
132
+ account = NEAR::Account.parse(account)
133
133
  stdout, stderr = execute(
134
134
  'account',
135
- 'delete-account', account_id,
136
- 'beneficiary', beneficiary_id,
135
+ 'delete-account', account.to_s,
136
+ 'beneficiary', (beneficiary || account.parent).to_s,
137
137
  'network-config', @network,
138
138
  'sign-with-keychain',
139
139
  'send'
@@ -1,21 +1,24 @@
1
1
  # This is free and unencumbered software released into the public domain.
2
2
 
3
+ require "base64"
4
+ require "pathname"
5
+
3
6
  ##
4
7
  # @see https://github.com/near/near-cli-rs/blob/main/docs/GUIDE.en.md#contract---Manage-smart-contracts-deploy-code-call-functions
5
8
  module NEAR::CLI::Contract
6
9
  ##
7
10
  # Calls a view method on a contract (read-only).
8
11
  #
9
- # @param [String] contract_id
12
+ # @param [NEAR::Account] contract
10
13
  # @param [String] method_name
11
14
  # @param [Hash] args JSON arguments for the method
12
15
  # @param [Block, Integer, String, Symbol] block
13
16
  # @return [String] The method call result
14
- def view_call(contract_id, method_name, args = {}, block: :now)
17
+ def view_call(contract, method_name, args = {}, block: :now)
15
18
  stdout, _ = execute(
16
19
  'contract',
17
20
  'call-function',
18
- 'as-read-only', contract_id, method_name,
21
+ 'as-read-only', contract.to_s, method_name,
19
22
  'json-args', args.to_json,
20
23
  'network-config', @network,
21
24
  *block_args(block)
@@ -26,22 +29,31 @@ module NEAR::CLI::Contract
26
29
  ##
27
30
  # Calls a state-changing method on a contract.
28
31
  #
29
- # @param [String] contract_id
32
+ # @param [NEAR::Account] contract
30
33
  # @param [String] method_name
31
- # @param [Hash] args JSON arguments for the method
32
- # @param [String] signer_id Account that signs the transaction
33
- # @param [String] deposit Amount of NEAR to attach
34
- # @param [String] gas Amount of gas to attach
34
+ # @param [Hash, Array, String, Pathname] args Arguments for the method
35
+ # @param [NEAR::Account] signer Account that signs the transaction
36
+ # @param [NEAR::Balance] deposit Amount of NEAR to attach
37
+ # @param [NEAR::Gas, #to_s] gas Amount of gas to attach
35
38
  # @return [String] Transaction result
36
- def call_function(contract_id, method_name, args = {}, signer_id:, deposit: '0 NEAR', gas: '30 TGas')
39
+ def call_function(contract, method_name, args = {}, signer:, deposit: nil, gas: '100.0 Tgas')
40
+ args = case args
41
+ when Hash, Array then ['json-args', args.to_json]
42
+ when String then case
43
+ when args.ascii_only? then ['text-args', args]
44
+ else ['base64-args', Base64.strict_encode64(args)]
45
+ end
46
+ when Pathname then ['file-args', args.to_s]
47
+ else raise ArgumentError, "Invalid argument type: #{args.inspect}"
48
+ end
37
49
  stdout, stderr = execute(
38
50
  'contract',
39
51
  'call-function',
40
- 'as-transaction', contract_id, method_name,
41
- 'json-args', args.to_json,
42
- 'prepaid-gas', gas,
43
- 'attached-deposit', deposit,
44
- 'sign-as', signer_id,
52
+ 'as-transaction', contract.to_s, method_name.to_s,
53
+ *args,
54
+ 'prepaid-gas', gas.to_s,
55
+ 'attached-deposit', (deposit ? deposit.to_s : '0') + ' NEAR',
56
+ 'sign-as', signer.to_s,
45
57
  'network-config', @network,
46
58
  'sign-with-keychain',
47
59
  'send'
data/lib/near/cli.rb CHANGED
@@ -22,6 +22,8 @@ class NEAR::CLI
22
22
  class ExecutionError < Error; end
23
23
 
24
24
  def self.find_program
25
+ return ENV['NEAR_CLI'] if ENV['NEAR_CLI']
26
+
25
27
  # First, check `$HOME/.cargo/bin/near`:
26
28
  path = File.expand_path('~/.cargo/bin/near')
27
29
  return path if File.executable?(path)
@@ -37,7 +39,7 @@ class NEAR::CLI
37
39
 
38
40
  ##
39
41
  # @param [String, #to_s] path
40
- # @param [String, #to_s] network
42
+ # @param [Network, #to_s] network
41
43
  def initialize(path: self.class.find_program, network: NEAR_ENV)
42
44
  @path, @network = path.to_s, network.to_s
43
45
  end
@@ -61,6 +63,7 @@ class NEAR::CLI
61
63
  # @param [Array<String>] args
62
64
  # @return [Array<String>]
63
65
  def execute(*args)
66
+ #command = [@path, '--quiet', *args.map(&:to_s)] # TODO: near-cli-rs 0.19+
64
67
  command = [@path, *args.map(&:to_s)]
65
68
  puts command.join(' ') if false
66
69
  stdout, stderr, status = Open3.capture3(*command)
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
@@ -0,0 +1,17 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'network'
4
+
5
+ ##
6
+ # Represents the NEAR Mainnet.
7
+ #
8
+ # @see https://docs.near.org/concepts/basics/networks#mainnet
9
+ class NEAR::Mainnet < NEAR::Network
10
+ NEARDATA_URL = 'https://mainnet.neardata.xyz'.freeze
11
+
12
+ ##
13
+ # @return [void]
14
+ def initialize
15
+ super(:mainnet)
16
+ end
17
+ end # NEAR::Mainnet
@@ -0,0 +1,134 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require 'faraday'
4
+ require 'faraday/follow_redirects'
5
+ require 'faraday/retry'
6
+
7
+ require_relative 'error'
8
+
9
+ ##
10
+ # Represents a NEAR Protocol network.
11
+ #
12
+ # @see https://docs.near.org/concepts/basics/networks
13
+ class NEAR::Network
14
+ ##
15
+ # @param [Symbol, #to_sym] id
16
+ # @return [void]
17
+ def initialize(id)
18
+ @id = id.to_sym
19
+ end
20
+
21
+ ##
22
+ # @return [String]
23
+ def to_s; @id.to_s; end
24
+
25
+ ##
26
+ # @return [String]
27
+ def neardata_url
28
+ self.class.const_get(:NEARDATA_URL)
29
+ end
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
+
51
+ ##
52
+ # Fetches the block at the given height.
53
+ #
54
+ # The block data is fetched from the neardata.xyz API.
55
+ #
56
+ # @param [NEAR::Block, #to_i] block
57
+ # @return [NEAR::Block]
58
+ # @raise [NEAR::Error::InvalidBlock] if the block does not exist
59
+ def fetch(block)
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
132
+ end
133
+ end
134
+ end # NEAR::Testnet
@@ -0,0 +1,17 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'network'
4
+
5
+ ##
6
+ # Represents the NEAR Testnet.
7
+ #
8
+ # @see https://docs.near.org/concepts/basics/networks#testnet
9
+ class NEAR::Testnet < NEAR::Network
10
+ NEARDATA_URL = 'https://testnet.neardata.xyz'.freeze
11
+
12
+ ##
13
+ # @return [void]
14
+ def initialize
15
+ super(:testnet)
16
+ end
17
+ 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,8 +3,27 @@
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'
11
+ require_relative 'near/mainnet'
12
+ require_relative 'near/network'
13
+ require_relative 'near/testnet'
9
14
  require_relative 'near/transaction'
10
15
  require_relative 'near/version'
16
+
17
+ module NEAR
18
+ ##
19
+ # @return [NEAR::Testnet]
20
+ def self.testnet
21
+ @testnet ||= Testnet.new
22
+ end
23
+
24
+ ##
25
+ # @return [NEAR::Mainnet]
26
+ def self.mainnet
27
+ @mainnet ||= Mainnet.new
28
+ end
29
+ end
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arto Bendiken
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-19 00:00:00.000000000 Z
10
+ date: 2025-02-02 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,10 @@ 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
134
+ - lib/near/mainnet.rb
135
+ - lib/near/network.rb
136
+ - lib/near/testnet.rb
62
137
  - lib/near/transaction.rb
63
138
  - lib/near/version.rb
64
139
  homepage: https://github.com/dryruby/near.rb