xrpl-ruby 0.5.0 → 0.6.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.
@@ -0,0 +1,498 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eventmachine'
4
+ require 'faye/websocket'
5
+ require 'json'
6
+ require 'securerandom'
7
+ require 'timeout'
8
+
9
+ module XRPL
10
+ # Raised when the WebSocket connection fails to open (or errors) before it
11
+ # becomes ready to accept requests.
12
+ class ConnectionError < StandardError; end
13
+
14
+ # Raised when a submitted transaction fails or cannot be confirmed as
15
+ # included in a validated ledger.
16
+ class TransactionError < StandardError; end
17
+
18
+ class Client
19
+ MAINNET_URL = 'wss://s1.ripple.com'
20
+ TESTNET_URL = 'wss://s.altnet.rippletest.net:51233'
21
+ DEVNET_URL = 'wss://s.devnet.rippletest.net:51233'
22
+
23
+ NETWORK_URLS = {
24
+ 'mainnet' => MAINNET_URL,
25
+ 'testnet' => TESTNET_URL,
26
+ 'devnet' => DEVNET_URL
27
+ }.freeze
28
+
29
+ # Added to the current ledger index to set LastLedgerSequence during autofill.
30
+ LEDGER_OFFSET = 20
31
+ # Approximate seconds between validated ledgers; used when polling for finality.
32
+ LEDGER_CLOSE_TIME = 3
33
+ # Default fee (drops) if the server's fee cannot be determined.
34
+ DEFAULT_FEE_DROPS = 10
35
+
36
+ attr_reader :url, :connection
37
+
38
+ # @param url [String, Symbol] a network alias (:testnet/:mainnet/:devnet) or a WebSocket URL.
39
+ # @param logger [Logger, nil] optional logger for diagnostic messages. When nil
40
+ # (the default), the client stays silent — a library must not write to the
41
+ # host application's stdout uninvited. Pass e.g. +Logger.new($stdout)+ to opt in.
42
+ def initialize(url, logger: nil)
43
+ @url = resolve_url(url)
44
+ @connection = nil
45
+ @requests = {}
46
+ @open = false
47
+ @ready_queue = Queue.new
48
+ @logger = logger
49
+ end
50
+
51
+ # Opens the WebSocket connection.
52
+ #
53
+ # By default this is non-blocking (preserving the previous behaviour) and
54
+ # returns +self+. Pass <tt>wait: true</tt> (or use {#connect!}) to block
55
+ # until the socket is actually open, so a following request can't race with
56
+ # connection setup and hit "Not connected".
57
+ #
58
+ # @param wait [Boolean] block until the connection is open.
59
+ # @param timeout [Numeric] seconds to wait when +wait+ is true.
60
+ # @return [self]
61
+ def connect(wait: false, timeout: 10)
62
+ @open = false
63
+ @ready_queue = Queue.new
64
+
65
+ Thread.new { EM.run } unless EM.reactor_running?
66
+
67
+ EM.next_tick do
68
+ @connection = Faye::WebSocket::Client.new(@url)
69
+
70
+ @connection.on :open do |event|
71
+ @open = true
72
+ @ready_queue.push(:open)
73
+ log("Connected to #{@url}")
74
+ end
75
+
76
+ @connection.on :message do |event|
77
+ handle_message(JSON.parse(event.data))
78
+ end
79
+
80
+ @connection.on :error do |event|
81
+ @ready_queue.push([:error, event.message])
82
+ end
83
+
84
+ @connection.on :close do |event|
85
+ @open = false
86
+ @connection = nil
87
+ log("Connection closed: #{event.code} #{event.reason}")
88
+ end
89
+ end
90
+
91
+ wait_until_open(timeout: timeout) if wait
92
+ self
93
+ end
94
+
95
+ # Opens the connection and blocks until it is ready to accept requests.
96
+ #
97
+ # @param timeout [Numeric] seconds to wait for the socket to open.
98
+ # @return [self]
99
+ def connect!(timeout: 10)
100
+ connect(wait: true, timeout: timeout)
101
+ end
102
+
103
+ # @return [Boolean] whether the WebSocket connection is currently open.
104
+ def open?
105
+ @open
106
+ end
107
+
108
+ # Blocks the calling thread until the connection is open.
109
+ #
110
+ # @param timeout [Numeric] seconds to wait before giving up.
111
+ # @return [true] once the socket is open.
112
+ # @raise [XRPL::ConnectionError] if the connection reports an error first.
113
+ # @raise [Timeout::Error] if the socket does not open within +timeout+.
114
+ def wait_until_open(timeout: 10)
115
+ return true if @open
116
+
117
+ signal = Timeout.timeout(timeout) { @ready_queue.pop }
118
+ if signal.is_a?(Array) && signal.first == :error
119
+ raise ConnectionError, "WebSocket connection failed: #{signal.last}"
120
+ end
121
+
122
+ true
123
+ rescue Timeout::Error
124
+ raise Timeout::Error, "Connection did not open within #{timeout} seconds"
125
+ end
126
+
127
+ def disconnect
128
+ @connection&.close
129
+ end
130
+
131
+ def request(command, params = {})
132
+ id = SecureRandom.uuid
133
+ register_pending_request(id)
134
+ payload = {
135
+ id: id,
136
+ command: command
137
+ }.merge(params)
138
+
139
+ send_message(payload)
140
+ # TODO: Implement promise/future or callback for response
141
+ id
142
+ end
143
+
144
+ def request_with_response(command, params = {}, timeout: 10)
145
+ id = SecureRandom.uuid
146
+ queue = Queue.new
147
+ register_pending_request(id, queue: queue)
148
+
149
+ payload = {
150
+ id: id,
151
+ command: command
152
+ }.merge(params)
153
+
154
+ send_message(payload)
155
+
156
+ Timeout.timeout(timeout) { queue.pop }
157
+ rescue Timeout::Error
158
+ @requests.delete(id)
159
+ raise Timeout::Error, "Request timed out after #{timeout} seconds"
160
+ end
161
+
162
+ def request_with_retry(command, params = {}, max_attempts: 3, timeout: 10,
163
+ retry_exceptions: [RuntimeError, Timeout::Error], **keyword_params)
164
+ attempt = 0
165
+ request_params = keyword_params.empty? ? params : params.merge(keyword_params)
166
+
167
+ begin
168
+ attempt += 1
169
+ request_with_response(command, request_params, timeout: timeout)
170
+ rescue *retry_exceptions => error
171
+ raise error if attempt >= max_attempts
172
+
173
+ retry
174
+ end
175
+ end
176
+
177
+ def subscribe(**params)
178
+ request('subscribe', **params)
179
+ end
180
+
181
+ def unsubscribe(**params)
182
+ request('unsubscribe', **params)
183
+ end
184
+
185
+ def account_channels(**params)
186
+ request('account_channels', **params)
187
+ end
188
+
189
+ def account_currencies(**params)
190
+ request('account_currencies', **params)
191
+ end
192
+
193
+ def account_info(**params)
194
+ request('account_info', **params)
195
+ end
196
+
197
+ def account_info_response(**params)
198
+ request_with_retry('account_info', params)
199
+ end
200
+
201
+ def account_lines(**params)
202
+ request('account_lines', **params)
203
+ end
204
+
205
+ def account_nfts(**params)
206
+ request('account_nfts', **params)
207
+ end
208
+
209
+ def account_objects(**params)
210
+ request('account_objects', **params)
211
+ end
212
+
213
+ def account_offers(**params)
214
+ request('account_offers', **params)
215
+ end
216
+
217
+ def account_tx(**params)
218
+ request('account_tx', **params)
219
+ end
220
+
221
+ def account_tx_response(**params)
222
+ request_with_retry('account_tx', params)
223
+ end
224
+
225
+ def account_tx_all(**params)
226
+ page_limit = params.delete(:page_limit)
227
+ max_attempts = params.delete(:max_attempts) || 3
228
+ timeout = params.delete(:timeout) || 10
229
+
230
+ current_params = params.dup
231
+ responses = []
232
+
233
+ loop do
234
+ response = request_with_retry('account_tx', current_params, max_attempts: max_attempts, timeout: timeout)
235
+ responses << response
236
+
237
+ marker = response.dig('result', 'marker')
238
+ break unless marker
239
+ break if page_limit && responses.size >= page_limit
240
+
241
+ current_params = current_params.merge(marker: marker)
242
+ end
243
+
244
+ responses
245
+ end
246
+
247
+ def summarize_account_tx(response)
248
+ result = response.fetch('result', {})
249
+ transactions = Array(result['transactions'])
250
+
251
+ {
252
+ 'ledger_index_min' => result['ledger_index_min'],
253
+ 'ledger_index_max' => result['ledger_index_max'],
254
+ 'transaction_count' => transactions.size,
255
+ 'validated' => result['validated'] == true,
256
+ 'marker_present' => !result['marker'].nil?
257
+ }
258
+ end
259
+
260
+ def gateway_balances(**params)
261
+ request('gateway_balances', **params)
262
+ end
263
+
264
+ def noripple_check(**params)
265
+ request('noripple_check', **params)
266
+ end
267
+
268
+ def ledger(**params)
269
+ request('ledger', **params)
270
+ end
271
+
272
+ def ledger_closed(**params)
273
+ request('ledger_closed', **params)
274
+ end
275
+
276
+ def ledger_current(**params)
277
+ request('ledger_current', **params)
278
+ end
279
+
280
+ def ledger_data(**params)
281
+ request('ledger_data', **params)
282
+ end
283
+
284
+ def ledger_entry(**params)
285
+ request('ledger_entry', **params)
286
+ end
287
+
288
+ def fee(**params)
289
+ request('fee', **params)
290
+ end
291
+
292
+ def fee_response(**params)
293
+ request_with_retry('fee', params)
294
+ end
295
+
296
+ def tx(**params)
297
+ request('tx', **params)
298
+ end
299
+
300
+ def tx_response(**params)
301
+ request_with_retry('tx', params)
302
+ end
303
+
304
+ # --- Transaction lifecycle (client-centric orchestration; see ADR-001) ---
305
+
306
+ # Fills in the fields a transaction needs before signing: +Sequence+, +Fee+
307
+ # and +LastLedgerSequence+. Existing values are never overwritten.
308
+ #
309
+ # @param transaction [Hash] the (string-keyed) transaction to complete.
310
+ # @param signers_count [Integer] number of signatures for multisign fee scaling.
311
+ # @return [Hash] a copy of the transaction with the missing fields filled in.
312
+ def autofill(transaction, signers_count: 0)
313
+ tx = transaction.dup
314
+ tx['Sequence'] ||= fetch_sequence(tx.fetch('Account'))
315
+ tx['Fee'] ||= calculate_fee(signers_count)
316
+ tx['LastLedgerSequence'] ||= current_ledger_index + LEDGER_OFFSET
317
+ tx
318
+ end
319
+
320
+ # Autofills (optional), signs with the given wallet and submits a transaction.
321
+ #
322
+ # @param transaction [Hash] the transaction to submit.
323
+ # @param wallet [Wallet::Wallet] wallet used to sign.
324
+ # @param autofill [Boolean] whether to autofill missing fields first.
325
+ # @param fail_hard [Boolean] reject the transaction rather than queueing it.
326
+ # @return [Hash] the raw +submit+ response.
327
+ def submit(transaction, wallet:, autofill: true, fail_hard: false)
328
+ prepared = prepare_for_submit(transaction, wallet: wallet, autofill: autofill)
329
+ submit_blob(prepared[:tx_blob], fail_hard: fail_hard)
330
+ end
331
+
332
+ # Like {#submit}, but then polls the ledger until the transaction is final
333
+ # (included in a validated ledger, or definitively failed/expired).
334
+ #
335
+ # @param transaction [Hash] the transaction to submit.
336
+ # @param wallet [Wallet::Wallet] wallet used to sign.
337
+ # @param autofill [Boolean] whether to autofill missing fields first.
338
+ # @param fail_hard [Boolean] reject the transaction rather than queueing it.
339
+ # @param timeout [Numeric] max seconds to wait for validation.
340
+ # @return [Hash] the validated +tx+ response.
341
+ # @raise [XRPL::TransactionError] if the transaction fails, expires or times out.
342
+ def submit_and_wait(transaction, wallet:, autofill: true, fail_hard: false, timeout: 20)
343
+ prepared = prepare_for_submit(transaction, wallet: wallet, autofill: autofill)
344
+ last_ledger = prepared[:tx]['LastLedgerSequence']
345
+ unless last_ledger
346
+ raise ArgumentError, 'Transaction must contain a LastLedgerSequence for reliable submission'
347
+ end
348
+
349
+ response = submit_blob(prepared[:tx_blob], fail_hard: fail_hard)
350
+ preliminary = response.dig('result', 'engine_result')
351
+
352
+ wait_for_final_outcome(prepared[:hash], last_ledger, preliminary, timeout: timeout)
353
+ end
354
+
355
+ private
356
+
357
+ def resolve_url(input)
358
+ return input unless input.is_a?(Symbol) || input.is_a?(String)
359
+
360
+ network_key = input.to_s
361
+ return NETWORK_URLS[network_key] if NETWORK_URLS.key?(network_key)
362
+
363
+ return input if network_key.include?('://')
364
+
365
+ if input.is_a?(Symbol)
366
+ raise ArgumentError, "Unsupported network alias: #{input}"
367
+ end
368
+
369
+ input
370
+ end
371
+
372
+ def send_message(payload)
373
+ raise "Not connected" unless @connection
374
+ @connection.send(payload.to_json)
375
+ end
376
+
377
+ def handle_message(message)
378
+ message_id = message['id']
379
+ return unless message_id
380
+ return unless @requests.key?(message_id)
381
+
382
+ request_entry = @requests[message_id]
383
+ request_entry[:queue]&.push(message) if request_entry.is_a?(Hash)
384
+ @requests.delete(message_id)
385
+ end
386
+
387
+ def register_pending_request(id, queue: nil)
388
+ @requests[id] = queue ? { queue: queue } : :pending
389
+ end
390
+
391
+ # Emits a diagnostic message via the injected logger, or stays silent when
392
+ # none was provided. The client never writes to stdout on its own.
393
+ def log(message)
394
+ @logger&.info(message)
395
+ end
396
+
397
+ # --- Transaction lifecycle helpers ---
398
+
399
+ def prepare_for_submit(transaction, wallet:, autofill:)
400
+ raise ArgumentError, 'wallet: is required to sign the transaction' if wallet.nil?
401
+
402
+ tx = transaction.is_a?(Hash) ? transaction.dup : transaction
403
+ tx = autofill(tx) if autofill && tx.is_a?(Hash)
404
+
405
+ signed = wallet.sign(tx)
406
+ { tx: tx, tx_blob: signed['tx_blob'], hash: signed['hash'] }
407
+ end
408
+
409
+ def submit_blob(tx_blob, fail_hard:, timeout: 10)
410
+ # request_with_response has a keyword parameter (timeout:). If we pass the
411
+ # params as a *trailing* hash without also filling the keyword slot, Ruby 3
412
+ # and RSpec's verifying doubles treat that hash as keyword arguments and
413
+ # reject it ("Invalid keyword arguments"). We therefore mirror the exact,
414
+ # proven call shape used by #request_with_retry: a positional params
415
+ # variable followed by an explicit `timeout:` keyword. String keys are the
416
+ # JSON field names the `submit` command expects.
417
+ params = { 'tx_blob' => tx_blob, 'fail_hard' => fail_hard }
418
+ request_with_response('submit', params, timeout: timeout)
419
+ end
420
+
421
+ def fetch_sequence(account)
422
+ response = account_info_response(account: account, ledger_index: 'current')
423
+ sequence = response.dig('result', 'account_data', 'Sequence')
424
+ raise TransactionError, "Could not determine Sequence for #{account}" unless sequence
425
+
426
+ Integer(sequence)
427
+ end
428
+
429
+ def calculate_fee(signers_count)
430
+ base = base_fee_drops
431
+ total = signers_count.to_i.positive? ? base * (1 + signers_count.to_i) : base
432
+ total.to_s
433
+ end
434
+
435
+ def base_fee_drops
436
+ response = request_with_retry('fee')
437
+ drops = response.dig('result', 'drops', 'open_ledger_fee') ||
438
+ response.dig('result', 'drops', 'base_fee')
439
+ drops ? Integer(drops) : DEFAULT_FEE_DROPS
440
+ rescue StandardError
441
+ DEFAULT_FEE_DROPS
442
+ end
443
+
444
+ def current_ledger_index
445
+ response = request_with_retry('ledger_current')
446
+ index = response.dig('result', 'ledger_current_index')
447
+ raise TransactionError, 'Could not determine current ledger index' unless index
448
+
449
+ Integer(index)
450
+ end
451
+
452
+ # Polls until the transaction is final. Checks the transaction result FIRST
453
+ # (return once validated), then whether the ledger has passed
454
+ # LastLedgerSequence — the reverse of xrpl-php, which can wrongly raise even
455
+ # though the transaction validated. See ADR-001.
456
+ def wait_for_final_outcome(tx_hash, last_ledger, preliminary_result, timeout:)
457
+ deadline = monotonic_time + timeout
458
+
459
+ loop do
460
+ response = tx_lookup(tx_hash)
461
+ if response
462
+ error = response.dig('result', 'error') || response['error']
463
+ if error.nil?
464
+ return response if response.dig('result', 'validated')
465
+ elsif error != 'txnNotFound'
466
+ raise TransactionError,
467
+ "Transaction #{tx_hash} failed: #{error} (preliminary: #{preliminary_result})"
468
+ end
469
+ end
470
+
471
+ latest = current_ledger_index
472
+ if latest > last_ledger
473
+ raise TransactionError,
474
+ "Transaction #{tx_hash} did not validate: ledger #{latest} passed " \
475
+ "LastLedgerSequence #{last_ledger} (preliminary: #{preliminary_result})"
476
+ end
477
+
478
+ if monotonic_time >= deadline
479
+ raise TransactionError,
480
+ "Timed out after #{timeout}s waiting for #{tx_hash} to validate " \
481
+ "(preliminary: #{preliminary_result})"
482
+ end
483
+
484
+ sleep LEDGER_CLOSE_TIME
485
+ end
486
+ end
487
+
488
+ def tx_lookup(tx_hash)
489
+ request_with_retry('tx', { transaction: tx_hash })
490
+ rescue StandardError
491
+ nil
492
+ end
493
+
494
+ def monotonic_time
495
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
496
+ end
497
+ end
498
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ require_relative 'client'
8
+
9
+ module XRPL
10
+ # Raised when a faucet request fails or funding does not complete in time.
11
+ class FaucetError < StandardError; end
12
+
13
+ # Funds (and thereby activates) a wallet on an XRP Ledger test network via the
14
+ # public faucet, then waits until the account shows up on the ledger.
15
+ #
16
+ # This is the Ruby equivalent of the +fundWallet()+ helper used in the other
17
+ # XRPL SDKs. It is intended for Testnet/Devnet only.
18
+ #
19
+ # @example Fund a fresh wallet on the Testnet
20
+ # client = XRPL::Client.new(:testnet)
21
+ # client.connect!
22
+ # result = XRPL.fund_wallet(client)
23
+ # result[:wallet].classic_address # => "r..."
24
+ # result[:balance] # => 100000000000 (drops)
25
+ class Faucet
26
+ TESTNET_FAUCET = 'https://faucet.altnet.rippletest.net/accounts'
27
+ DEVNET_FAUCET = 'https://faucet.devnet.rippletest.net/accounts'
28
+
29
+ DEFAULT_TIMEOUT = 40
30
+ DEFAULT_POLL_INTERVAL = 2
31
+
32
+ attr_reader :faucet_url
33
+
34
+ # @param faucet_url [String] the faucet endpoint to POST to.
35
+ def initialize(faucet_url: TESTNET_FAUCET)
36
+ @faucet_url = faucet_url
37
+ end
38
+
39
+ # Convenience wrapper matching the tutorial call style.
40
+ #
41
+ # @param client [XRPL::Client] a connected client used to poll for funding.
42
+ # @param wallet [Wallet::Wallet, nil] wallet to fund; a new one is generated when nil.
43
+ # @return [Hash] +{ wallet:, balance: }+ (balance in drops).
44
+ def self.fund_wallet(client, wallet = nil, faucet_url: TESTNET_FAUCET, **opts)
45
+ new(faucet_url: faucet_url).fund_wallet(client, wallet, **opts)
46
+ end
47
+
48
+ # Funds (and activates) a wallet on a test network.
49
+ #
50
+ # @param client [XRPL::Client] a connected client used to poll for funding.
51
+ # @param wallet [Wallet::Wallet, nil] wallet to fund; a new one is generated when nil.
52
+ # @param amount [Numeric, String, nil] optional XRP amount to request.
53
+ # @param timeout [Numeric] seconds to wait for the account to be funded.
54
+ # @param poll_interval [Numeric] seconds between ledger polls.
55
+ # @return [Hash] +{ wallet:, balance: }+ (balance in drops).
56
+ # @raise [XRPL::FaucetError] if the faucet request fails or funding times out.
57
+ def fund_wallet(client, wallet = nil, amount: nil, timeout: DEFAULT_TIMEOUT,
58
+ poll_interval: DEFAULT_POLL_INTERVAL)
59
+ wallet ||= Wallet::Wallet.generate
60
+
61
+ request_funds(wallet.classic_address, amount)
62
+ balance = await_funding(
63
+ client, wallet.classic_address,
64
+ timeout: timeout, poll_interval: poll_interval
65
+ )
66
+
67
+ { wallet: wallet, balance: balance }
68
+ end
69
+
70
+ private
71
+
72
+ # POSTs a funding request to the faucet for the given address.
73
+ def request_funds(address, amount)
74
+ body = { destination: address }
75
+ body[:xrpAmount] = amount.to_s if amount
76
+
77
+ http_post(@faucet_url, body)
78
+ end
79
+
80
+ # Polls the ledger until the account is funded, i.e. it exists and shows a
81
+ # positive balance. (For a Testnet/get-started helper this simple
82
+ # "funded = has a balance" rule is robust: unlike waiting for a strict
83
+ # balance *increase*, it can never hang when a wallet already holds XRP.)
84
+ def await_funding(client, address, timeout:, poll_interval:)
85
+ deadline = monotonic_time + timeout
86
+
87
+ loop do
88
+ balance = current_balance(client, address)
89
+ return balance if balance && balance.positive?
90
+
91
+ if monotonic_time >= deadline
92
+ raise FaucetError, "Account #{address} was not funded within #{timeout} seconds"
93
+ end
94
+
95
+ sleep poll_interval
96
+ end
97
+ end
98
+
99
+ # Returns the account balance in drops, or nil if the account is not (yet)
100
+ # found or a transient lookup error occurs.
101
+ def current_balance(client, address)
102
+ response = client.account_info_response(account: address, ledger_index: 'validated')
103
+ drops = response.dig('result', 'account_data', 'Balance')
104
+ drops && Integer(drops)
105
+ rescue StandardError
106
+ nil
107
+ end
108
+
109
+ def http_post(url, body)
110
+ uri = URI(url)
111
+ http = Net::HTTP.new(uri.host, uri.port)
112
+ http.use_ssl = uri.scheme == 'https'
113
+
114
+ request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
115
+ request.body = JSON.generate(body)
116
+
117
+ response = http.request(request)
118
+ unless response.is_a?(Net::HTTPSuccess)
119
+ raise FaucetError, "Faucet request failed: #{response.code} #{response.message}"
120
+ end
121
+
122
+ JSON.parse(response.body)
123
+ rescue JSON::ParserError
124
+ {}
125
+ end
126
+
127
+ def monotonic_time
128
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
+ end
130
+ end
131
+
132
+ # Top-level convenience mirroring the other SDKs' +fundWallet()+ sugar.
133
+ #
134
+ # @see Faucet#fund_wallet
135
+ def self.fund_wallet(client, wallet = nil, **opts)
136
+ Faucet.fund_wallet(client, wallet, **opts)
137
+ end
138
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ VERSION = '0.6.0'
5
+ end
data/lib/xrpl-ruby.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'xrpl/version'
4
+
3
5
  require_relative 'core/base_x'
4
6
  require_relative 'core/base_58_xrp'
5
7
  require_relative 'core/core'
@@ -35,4 +37,7 @@ require_relative 'wallet/wallet'
35
37
 
36
38
  require_relative 'key-pairs/ed25519'
37
39
  require_relative 'key-pairs/secp256k1'
38
- require_relative 'key-pairs/key_pairs'
40
+ require_relative 'key-pairs/key_pairs'
41
+
42
+ require_relative 'xrpl/client'
43
+ require_relative 'xrpl/faucet'