banano 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ module Banano
7
+ class Client
8
+ LOCAL_ENDPOINT = 'http://localhost:7072'
9
+ DEFAULT_TIMEOUT = 30
10
+
11
+ attr_accessor :uri, :timeout
12
+
13
+ def initialize(uri: LOCAL_ENDPOINT, timeout: DEFAULT_TIMEOUT)
14
+ @conn = Faraday.new(uri) do |builder|
15
+ builder.adapter Faraday.default_adapter
16
+ builder.request :url_encoded
17
+ builder.options[:open_timeout] = 5
18
+ builder.options[:timeout] = timeout
19
+ builder.headers['Content-Type'] = 'application/json'
20
+ builder.headers['User-Agent'] = 'Banano RPC Client'
21
+ builder.response :json, content_type: 'application/json'
22
+ end
23
+ end
24
+
25
+ def rpc_call(action:, params: {})
26
+ data = {action: action}.merge(params)
27
+ response = @conn.post do |req|
28
+ req.body = JSON.dump(data)
29
+ end
30
+ Util.symbolize_keys(response.body)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Banano
4
+ # Standard error for Banano
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Banano
4
+ class Key
5
+ def initialize(node:, key: nil)
6
+ @node = node
7
+ @key = key
8
+ end
9
+
10
+ def generate(seed: nil, index: nil)
11
+ if seed.nil? && index.nil?
12
+ rpc(action: :key_create)
13
+ elsif !seed.nil? && !index.nil?
14
+ rpc(action: :deterministic_key, params: {seed: seed, index: index})
15
+ else
16
+ raise ArgumentError, "Method must be called with either seed AND index params"
17
+ end
18
+ end
19
+
20
+ # Derive public key and account from private key
21
+ def expand
22
+ return {} if @key.nil?
23
+
24
+ rpc(action: :key_expand, params: {key: @key})
25
+ end
26
+
27
+ def id
28
+ @key
29
+ end
30
+
31
+ def info
32
+ key_required!
33
+ rpc(action: :key_expand)
34
+ end
35
+
36
+ private
37
+
38
+ def rpc(action:, params: {})
39
+ p = @key.nil? ? {} : {key: @key}
40
+ @node.rpc(action: action, params: p.merge(params))
41
+ end
42
+
43
+ def key_required!
44
+ raise ArgumentError, "Key must be present" if @key.nil?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Banano
4
+ class Node
5
+ attr_reader :uri, :timeout
6
+
7
+ def initialize(uri: Client::LOCAL_ENDPOINT, timeout: Client::DEFAULT_TIMEOUT)
8
+ @client = Client.new(uri: uri, timeout: timeout)
9
+ end
10
+
11
+ def rpc(action:, params: {})
12
+ @client.rpc_call(action: action, params: params)
13
+ end
14
+
15
+ # The number of accounts in the nano ledger--essentially all
16
+ # accounts with _open_ blocks. An _open_ block
17
+ # is the type of block written to the nano ledger when an account
18
+ # receives its first payment (see {Nanook::WalletAccount#receive}). All accounts
19
+ # that respond +true+ to {Nanook::Account#exists?} have open blocks in the ledger.
20
+ #
21
+ # @return [Integer] number of accounts with _open_ blocks.
22
+ def account_count
23
+ rpc(action: :frontier_count)[:count]
24
+ end
25
+ alias frontier_count account_count
26
+
27
+ # The count of all blocks downloaded to the node, and
28
+ # blocks still to be synchronized by the node.
29
+ #
30
+ # @return [Hash{Symbol=>Integer}] number of blocks and unchecked
31
+ # synchronizing blocks
32
+ def block_count
33
+ rpc(action: :block_count)
34
+ end
35
+
36
+ # The count of all known blocks by their type.
37
+ #
38
+ # @return [Hash{Symbol=>Integer}] number of blocks by type
39
+ def block_count_by_type
40
+ rpc(action: :block_count_type)
41
+ end
42
+ alias block_count_type block_count_by_type
43
+
44
+ # TODO: add bootstrap methods
45
+
46
+ # @return [Hash{Symbol=>String}] information about the node peers
47
+ def peers
48
+ rpc(action: :peers)[:peers]
49
+ end
50
+
51
+ # All representatives and their voting weight.
52
+ #
53
+ # @param raw [Boolean] if true return raw balances, else banano units
54
+ # @return [Hash{Symbol=>Integer}] known representatives and their voting weight
55
+ def representatives(raw = true)
56
+ response = rpc(action: :representatives)[:representatives]
57
+ return response if raw == true
58
+
59
+ r = response.map do |address, balance|
60
+ [address.to_s, Banano::Unit.raw_to_ban(balance).to_f]
61
+ end
62
+ Hash[r]
63
+ end
64
+
65
+ # All online representatives that have voted recently. Note, due to the
66
+ # design of the nano RPC, this method cannot return the voting weight
67
+ # of the representatives.
68
+ #
69
+ # ==== Example:
70
+ #
71
+ # node.representatives_online # => ["ban_111...", "ban_311..."]
72
+ #
73
+ # @return [Array<String>] array of representative account ids
74
+ def representatives_online
75
+ rpc(action: :representatives_online)[:representatives]
76
+ end
77
+ alias reps_online representatives_online
78
+
79
+ # @param limit [Integer] number of synchronizing blocks to return
80
+ # @return [Hash{Symbol=>String}] information about the synchronizing blocks for this node
81
+ def synchronizing_blocks(limit: 1000)
82
+ response = rpc(action: :unchecked, params: {count: limit})[:blocks]
83
+ # response = response.map do |block, info|
84
+ # [block, JSON.parse(info).to_symbolized_hash]
85
+ # end
86
+ # Hash[response.sort].to_symbolized_hash
87
+ response
88
+ end
89
+ alias unchecked synchronizing_blocks
90
+
91
+ # The percentage completeness of the synchronization process for
92
+ # your node as it downloads the nano ledger. Note, it's normal for
93
+ # your progress to not ever reach 100. The closer to 100, the more
94
+ # complete your node's data is, and so the query methods in this class
95
+ # become more reliable.
96
+ #
97
+ # @return [Float] the percentage completeness of the synchronization
98
+ # process for your node
99
+ def sync_progress
100
+ response = block_count
101
+
102
+ count = response[:count].to_i
103
+ unchecked = response[:unchecked].to_i
104
+ total = count + unchecked
105
+
106
+ count.to_f * 100 / total.to_f
107
+ end
108
+
109
+ # @return [Hash{Symbol=>Integer|String}] version information for this node
110
+ def version
111
+ rpc(action: :version)
112
+ end
113
+ alias info version
114
+ end
115
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module Banano
6
+ class Unit
7
+ # Constant used to convert back and forth between raw and banano
8
+ STEP = BigDecimal(10)**29
9
+ TOTAL = (BigDecimal(2)**128 - 1).to_i
10
+
11
+ # Converts an amount of banano to an amount of raw.
12
+ #
13
+ # @param banano [Float|Integer] amount in banano
14
+ # @return [Integer] amount in raw
15
+ def self.ban_to_raw(banano)
16
+ return 0 unless banano.is_a?(Numeric) && banano > 0
17
+
18
+ result = (banano * STEP).to_i
19
+ return 0 if result > TOTAL
20
+
21
+ result
22
+ end
23
+
24
+ # Converts an amount of raw to an amount of banano
25
+ #
26
+ # @param raw [BigDecimal|String] amount in raw
27
+ # @return [Float|Integer] amount in banano
28
+ def self.raw_to_ban(raw)
29
+ return 0 unless raw.is_a?(BigDecimal) || raw.is_a?(String)
30
+
31
+ begin
32
+ value = raw.is_a?(String) ? BigDecimal(raw) : raw
33
+ return 0 if value < 1.0 || value > TOTAL
34
+
35
+ value / STEP
36
+ rescue ArgumentError
37
+ 0
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Banano
4
+ class Util
5
+ class << self
6
+ def symbolize_keys(hash)
7
+ return {} if hash.empty?
8
+
9
+ converted = hash.is_a?(String) ? JSON.parse(hash) : hash
10
+ converted.inject({}) do |result, (key, value)|
11
+ new_key = case key
12
+ when String then key.to_sym
13
+ else key
14
+ end
15
+ new_value = case value
16
+ when Hash then symbolize_keys(value)
17
+ else value
18
+ end
19
+ result[new_key] = new_value
20
+ result
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Banano
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The <tt>Banano::Wallet</tt> class lets you manage your banano wallets,
4
+ # as well as some account-specific things like making and receiving payments.
5
+ #
6
+ # === Wallet seeds vs ids
7
+ #
8
+ # Your wallets each have an id as well as a seed. Both are 32-byte uppercase hex
9
+ # strings that look like this:
10
+ #
11
+ # 000D1BAEC8EC208142C99059B393051BAC8380F9B5A2E6B2489A277D81789F3F
12
+ #
13
+ # This class uses wallet _ids_ to identify your wallet. A wallet id only
14
+ # exists locally on the banano node that it was created on. The person
15
+ # who knows this id can only perform all read and write actions against
16
+ # the wallet and all accounts inside the wallet from the same banano node
17
+ # that it was created on. This makes wallet ids fairly safe to use as a
18
+ # person needs to know your wallet id as well as have access to run
19
+ # RPC commands against your banano node to be able to control your accounts.
20
+ #
21
+ # A _seed_ on the other hand can be used to link any wallet to another
22
+ # wallet's accounts, from anywhere in the banano network. This happens
23
+ # by setting a wallet's seed to be the same as a previous wallet's seed.
24
+ # When a wallet has the same seed as another wallet, any accounts
25
+ # created in the second wallet will be the same accounts as those that were
26
+ # created in the previous wallet, and the new wallet's owner will
27
+ # also gain ownership of the previous wallet's accounts. Note, that the
28
+ # two wallets will have different ids, but the same seed.
29
+ #
30
+
31
+ module Banano
32
+ class Wallet
33
+ attr_reader :node
34
+
35
+ def initialize(node:, wallet: nil)
36
+ @node = node
37
+ @wallet = wallet
38
+ end
39
+
40
+ # Changes a wallet's seed.
41
+ #
42
+ # It's recommended to only change the seed of a wallet that contains
43
+ # no accounts.
44
+ #
45
+ # ==== Example:
46
+ #
47
+ # wallet.change_seed("000D1BA...") # => true
48
+ #
49
+ # @param seed [String] the seed to change to.
50
+ # @return [Boolean] indicating whether the change was successful.
51
+ def change_seed(seed)
52
+ wallet_required!
53
+ rpc(action: :wallet_change_seed, params: {seed: seed}).key?(:success)
54
+ end
55
+
56
+ # Returns the given account in the wallet as a {Banano::WalletAccount} instance
57
+ # to let you start working with it.
58
+ #
59
+ # Call with no +account+ argument if you wish to create a new account
60
+ # in the wallet, like this:
61
+ #
62
+ # wallet.account.create # => Banano::WalletAccount
63
+ #
64
+ # See {Banano::WalletAccount} for all the methods you can call on the
65
+ # account object returned.
66
+ #
67
+ # ==== Examples:
68
+ #
69
+ # wallet.account("nano_...") # => Banano::WalletAccount
70
+ # wallet.account.create # => Banano::WalletAccount
71
+ #
72
+ # @param [String] account optional String of an account (starting with
73
+ # <tt>"xrb..."</tt>) to start working with. Must be an account within
74
+ # the wallet. When no account is given, the instance returned only
75
+ # allows you to call +create+ on it, to create a new account.
76
+ # @raise [ArgumentError] if the wallet does no contain the account
77
+ # @return [Banano::WalletAccount]
78
+ def account(account = nil)
79
+ Banano::WalletAccount.new(node: @node, wallet: @wallet, account: account)
80
+ end
81
+
82
+ # Array of {Banano::WalletAccount} instances of accounts in the wallet.
83
+ #
84
+ # ==== Example:
85
+ #
86
+ # wallet.accounts # => [Banano::WalletAccount, Banano::WalletAccount...]
87
+ #
88
+ # @return [Array<Banano::WalletAccount>] all accounts in the wallet
89
+ def accounts
90
+ wallet_required!
91
+ rpc(action: :account_list)[:accounts]
92
+ end
93
+
94
+ # Will return +true+ if the account exists in the wallet.
95
+ #
96
+ # ==== Example:
97
+ # wallet.contains?("ban_1...") # => true
98
+ #
99
+ # @param account [String] id (will start with <tt>"ban_..."</tt>)
100
+ # @return [Boolean] indicating if the wallet contains the given account
101
+ # TODO: account address validation - Maybe Banano::Address ....
102
+ def contains?(account)
103
+ wallet_required!
104
+ response = rpc(action: :wallet_contains, params: {account: account})
105
+ !response.empty? && response[:exists] == '1'
106
+ end
107
+
108
+ # Creates a new wallet.
109
+ #
110
+ # The wallet will be created only on this node. It's important that
111
+ # if you intend to add funds to accounts in this wallet that you
112
+ # backup the wallet *seed* in order to restore the wallet in future.
113
+ #
114
+ # ==== Example:
115
+ # Banano::Wallet.new.create # => Banano::Wallet
116
+ #
117
+ # @return [Banano::Wallet]
118
+ def create
119
+ @wallet = rpc(action: :wallet_create)[:wallet]
120
+ self
121
+ end
122
+
123
+ # Destroys the wallet.
124
+ #
125
+ # ==== Example:
126
+ #
127
+ # wallet.destroy # => true
128
+ #
129
+ # @return [Boolean] indicating success of the action
130
+ def destroy
131
+ wallet_required!
132
+ rpc(action: :wallet_destroy)
133
+ @wallet = nil
134
+ true
135
+ end
136
+
137
+ # Generates a String containing a JSON representation of your wallet.
138
+ #
139
+ def export
140
+ wallet_required!
141
+ rpc(action: :wallet_export)[:json]
142
+ end
143
+
144
+ # The default representative account id for the wallet. This is the
145
+ # representative that all new accounts created in this wallet will have.
146
+ #
147
+ # Changing the default representative for a wallet does not change
148
+ # the representatives for any accounts that have been created.
149
+ #
150
+ # ==== Example:
151
+ #
152
+ # wallet.default_representative # => "ban_3pc..."
153
+ #
154
+ # @return [String] Representative account of the account
155
+ def default_representative
156
+ rpc(action: :wallet_representative)[:representative]
157
+ end
158
+ alias representative default_representative
159
+
160
+ # Sets the default representative for the wallet. A wallet's default
161
+ # representative is the representative all new accounts created in
162
+ # the wallet will have. Changing the default representative for a
163
+ # wallet does not change the representatives for existing accounts
164
+ # in the wallet.
165
+ #
166
+ # ==== Example:
167
+ #
168
+ # wallet.change_default_representative("ban_...") # => "ban_..."
169
+ #
170
+ # @param [String] representative the id of the representative account
171
+ # to set as this account's representative
172
+ # @return [String] the representative account id
173
+ # @raise [ArgumentError] if the representative account does not exist
174
+ # @raise [Banano::Error] if setting the representative fails
175
+ def change_default_representative(representative)
176
+ unless Banano::Account.new(node: @node, address: representative).exists?
177
+ raise ArgumentError, "Representative account does not exist: #{representative}"
178
+ end
179
+
180
+ if rpc(action: :wallet_representative_set,
181
+ params: {representative: representative})[:set] == '1'
182
+ representative
183
+ else
184
+ raise Banano::Error, "Setting the representative failed"
185
+ end
186
+ end
187
+ alias change_representative change_default_representative
188
+
189
+ # @return [String] the wallet id
190
+ def id
191
+ @wallet
192
+ end
193
+
194
+ # Information about this wallet and all of its accounts.
195
+ #
196
+ # ==== Examples:
197
+ #
198
+ # wallet.info
199
+ #
200
+ # @param raw [Boolean] if true return raw, else return ban units
201
+ # @return [Hash{Symbol=>String|Array<Hash{Symbol=>String|Integer|Float}>}]
202
+ # information about the wallet.
203
+ def info
204
+ wallet_required!
205
+ rpc(action: :wallet_info)
206
+ end
207
+
208
+ # Locks the wallet. A locked wallet cannot pocket pending transactions or make payments.
209
+ #
210
+ # ==== Example:
211
+ #
212
+ # wallet.lock #=> true
213
+ #
214
+ # @return [Boolean] indicates if the wallet was successfully locked
215
+ def lock
216
+ wallet_required!
217
+ response = rpc(action: :wallet_lock)
218
+ !response.empty? && response[:locked] == '1'
219
+ end
220
+
221
+ # Returns +true+ if the wallet is locked.
222
+ #
223
+ # ==== Example:
224
+ #
225
+ # wallet.locked? #=> false
226
+ #
227
+ # @return [Boolean] indicates if the wallet is locked
228
+ def locked?
229
+ wallet_required!
230
+ response = rpc(action: :wallet_locked)
231
+ !response.empty? && response[:locked] != '0'
232
+ end
233
+
234
+ # Unlocks a previously locked wallet.
235
+ #
236
+ # ==== Example:
237
+ #
238
+ # wallet.unlock("new_pass") #=> true
239
+ #
240
+ # @return [Boolean] indicates if the unlocking action was successful
241
+ def unlock(password)
242
+ wallet_required!
243
+ rpc(action: :password_enter, params: {password: password})[:valid] == '1'
244
+ end
245
+
246
+ # Changes the password for a wallet.
247
+ #
248
+ # ==== Example:
249
+ #
250
+ # wallet.change_password("new_pass") #=> true
251
+ # @return [Boolean] indicates if the action was successful
252
+ def change_password(password)
253
+ wallet_required!
254
+ rpc(action: :password_change, params: {password: password})[:changed] == '1'
255
+ end
256
+
257
+ # Balance of all accounts in the wallet, optionally breaking the balances down by account.
258
+ #
259
+ # ==== Examples:
260
+ # wallet.balance
261
+ #
262
+ # Example response:
263
+ #
264
+ # {
265
+ # "balance"=>5,
266
+ # "pending"=>0.001
267
+ # }
268
+ #
269
+ # @param [Boolean] account_break_down (default is +false+). When +true+
270
+ # the response will contain balances per account.
271
+ # @param raw [Boolean] raw or banano units
272
+ #
273
+ # @return [Hash{Symbol=>Integer|Float|Hash}]
274
+ def balance(account_break_down: false, raw: true)
275
+ wallet_required!
276
+
277
+ if account_break_down
278
+ response = rpc(action: :wallet_balances)[:balances].tap do |r|
279
+ unless raw == true
280
+ r.each do |account, _|
281
+ r[account][:balance] = Banano::Unit.raw_to_ban(r[account][:balance]).to_f
282
+ r[account][:pending] = Banano::Unit.raw_to_ban(r[account][:pending]).to_f
283
+ end
284
+ end
285
+ end
286
+ return response.collect {|k, v| [k.to_s, v] }.to_h
287
+ end
288
+
289
+ rpc(action: :wallet_balance_total).tap do |r|
290
+ unless raw == true
291
+ r[:balance] = Banano::Unit.raw_to_ban(r[:balance]).to_f
292
+ r[:pending] = Banano::Unit.raw_to_ban(r[:pending]).to_f
293
+ end
294
+ end
295
+ end
296
+
297
+ # Makes a payment from an account in your wallet to another account
298
+ # on the nano network.
299
+ #
300
+ # Note, there may be a delay in receiving a response due to Proof of
301
+ # Work being done. From the {Nano RPC}[https://docs.nano.org/commands/rpc-protocol/#send]:
302
+ #
303
+ # <i>Proof of Work is precomputed for one transaction in the
304
+ # background. If it has been a while since your last transaction it
305
+ # will send instantly, the next one will need to wait for Proof of
306
+ # Work to be generated.</i>
307
+ #
308
+ # @param from [String] account id of an account in your wallet
309
+ # @param to (see Banano::WalletAccount#pay)
310
+ # @param amount (see Banano::WalletAccount#pay)
311
+ # @param raw [Boolean] raw or banano units
312
+ # @params id (see Banano::WalletAccount#pay)
313
+ #
314
+ # @return (see Banano::WalletAccount#pay)
315
+ # @raise [Banano::Error] if unsuccessful
316
+ def pay(from:, to:, amount:, raw: true, id:)
317
+ wallet_required!
318
+ validate_wallet_contains_account!(from)
319
+ # account(from) will return Banano::WalletAccount
320
+ account(from).pay(to: to, amount: amount, raw: raw, id: id)
321
+ end
322
+
323
+ # Information about pending blocks (payments) that are waiting
324
+ # to be received by accounts in this wallet.
325
+ #
326
+ # See also the {#receive} method of this class for how to receive a pending payment.
327
+ #
328
+ # @param limit [Integer] number of accounts with pending payments to return (default is 1000)
329
+ # @param detailed [Boolean] return complex Hash of pending block info (default is +false+)
330
+ # @param raw [Boolean] raw or banano units
331
+ #
332
+ # ==== Examples:
333
+ #
334
+ # wallet.pending
335
+ #
336
+ def pending(limit: 1000, detailed: false, raw: true)
337
+ wallet_required!
338
+ params = {count: limit}
339
+ params[:source] = true if detailed
340
+
341
+ response = rpc(action: :wallet_pending, params: params)[:blocks]
342
+ return response unless detailed && !response.empty?
343
+
344
+ # Map the RPC response, which is:
345
+ # account=>block=>[amount|source] into
346
+ # account=>[block|amount|source]
347
+ response.map do |account, data|
348
+ new_data = data.map do |block, amount_and_source|
349
+ d = amount_and_source.merge(block: block.to_s)
350
+ d[:amount] = Banano::Unit.raw_to_ban(d[:amount]) unless raw == true
351
+ d
352
+ end
353
+
354
+ [account, new_data]
355
+ end
356
+ end
357
+
358
+ # Receives a pending payment into an account in the wallet.
359
+ #
360
+ # When called with no +block+ argument, the latest pending payment
361
+ # for the account will be received.
362
+ #
363
+ # Returns a <i>receive</i> block hash id if a receive was successful,
364
+ # or +false+ if there were no pending payments to receive.
365
+ #
366
+ # You can receive a specific pending block if you know it by
367
+ # passing the block has in as an argument.
368
+ # ==== Examples:
369
+ #
370
+ # wallet.receive(into: "ban_..") # => "9AE2311..."
371
+ # wallet.receive("718CC21...", into: "ban_..") # => "9AE2311..."
372
+ #
373
+ # @param block (see Banano::WalletAccount#receive)
374
+ # @param into [String] account id of account in your wallet to receive the
375
+ # payment into
376
+ #
377
+ # @return (see Banano::WalletAccount#receive)
378
+ def receive(block: nil, into:)
379
+ wallet_required!
380
+ validate_wallet_contains_account!(into)
381
+ # account(into) will return Banano::WalletAccount
382
+ account(into).receive(block)
383
+ end
384
+
385
+ # Restores a previously created wallet by its seed.
386
+ # A new wallet will be created on your node (with a new wallet id)
387
+ # and will have its seed set to the given seed.
388
+ #
389
+ # ==== Example:
390
+ #
391
+ # Banano::Protocol.new.wallet.restore(seed) # => Nanook::Wallet
392
+ #
393
+ # @param seed [String] the wallet seed to restore.
394
+ # @param accounts [Integer] optionally restore the given number of accounts for the wallet.
395
+ #
396
+ # @return [Banano::Wallet] a new wallet
397
+ # @raise [Banano::Error] if unsuccessful
398
+ def restore(seed:, accounts: 0)
399
+ create
400
+
401
+ raise Nanook::Error, "Unable to set seed for wallet" unless change_seed(seed)
402
+
403
+ account.create(accounts) if accounts > 0
404
+ self
405
+ end
406
+
407
+ private
408
+
409
+ def rpc(action:, params: {})
410
+ p = @wallet.nil? ? {} : {wallet: @wallet}
411
+ @node.rpc(action: action, params: p.merge(params))
412
+ end
413
+
414
+ def wallet_required!
415
+ raise ArgumentError, "Wallet must be present" if @wallet.nil?
416
+ end
417
+
418
+ def validate_wallet_contains_account!(account)
419
+ @known_valid_accounts ||= []
420
+ return true if @known_valid_accounts.include?(account)
421
+
422
+ if contains?(account)
423
+ @known_valid_accounts << account
424
+ else
425
+ raise ArgumentError,
426
+ "Account does not exist in wallet. Account: #{account}, wallet: #{@wallet}"
427
+ end
428
+ end
429
+ end
430
+ end