Class: XRPL::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/xrpl/client.rb

Constant Summary collapse

MAINNET_URL =
'wss://s1.ripple.com'
TESTNET_URL =
'wss://s.altnet.rippletest.net:51233'
DEVNET_URL =
'wss://s.devnet.rippletest.net:51233'
NETWORK_URLS =
{
  'mainnet' => MAINNET_URL,
  'testnet' => TESTNET_URL,
  'devnet' => DEVNET_URL
}.freeze
LEDGER_OFFSET =

Added to the current ledger index to set LastLedgerSequence during autofill.

20
LEDGER_CLOSE_TIME =

Approximate seconds between validated ledgers; used when polling for finality.

3
DEFAULT_FEE_DROPS =

Default fee (drops) if the server's fee cannot be determined.

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, logger: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • url (String, Symbol)

    a network alias (:testnet/:mainnet/:devnet) or a WebSocket URL.

  • logger (Logger, nil) (defaults to: nil)

    optional logger for diagnostic messages. When nil (the default), the client stays silent — a library must not write to the host application's stdout uninvited. Pass e.g. Logger.new($stdout) to opt in.



42
43
44
45
46
47
48
49
# File 'lib/xrpl/client.rb', line 42

def initialize(url, logger: nil)
  @url = resolve_url(url)
  @connection = nil
  @requests = {}
  @open = false
  @ready_queue = Queue.new
  @logger = logger
end

Instance Attribute Details

#connectionObject (readonly)

Returns the value of attribute connection.



36
37
38
# File 'lib/xrpl/client.rb', line 36

def connection
  @connection
end

#urlObject (readonly)

Returns the value of attribute url.



36
37
38
# File 'lib/xrpl/client.rb', line 36

def url
  @url
end

Instance Method Details

#account_channels(**params) ⇒ Object



185
186
187
# File 'lib/xrpl/client.rb', line 185

def (**params)
  request('account_channels', **params)
end

#account_currencies(**params) ⇒ Object



189
190
191
# File 'lib/xrpl/client.rb', line 189

def (**params)
  request('account_currencies', **params)
end

#account_info(**params) ⇒ Object



193
194
195
# File 'lib/xrpl/client.rb', line 193

def (**params)
  request('account_info', **params)
end

#account_info_response(**params) ⇒ Object



197
198
199
# File 'lib/xrpl/client.rb', line 197

def (**params)
  request_with_retry('account_info', params)
end

#account_lines(**params) ⇒ Object



201
202
203
# File 'lib/xrpl/client.rb', line 201

def (**params)
  request('account_lines', **params)
end

#account_nfts(**params) ⇒ Object



205
206
207
# File 'lib/xrpl/client.rb', line 205

def (**params)
  request('account_nfts', **params)
end

#account_objects(**params) ⇒ Object



209
210
211
# File 'lib/xrpl/client.rb', line 209

def (**params)
  request('account_objects', **params)
end

#account_offers(**params) ⇒ Object



213
214
215
# File 'lib/xrpl/client.rb', line 213

def (**params)
  request('account_offers', **params)
end

#account_tx(**params) ⇒ Object



217
218
219
# File 'lib/xrpl/client.rb', line 217

def (**params)
  request('account_tx', **params)
end

#account_tx_all(**params) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/xrpl/client.rb', line 225

def (**params)
  page_limit = params.delete(:page_limit)
  max_attempts = params.delete(:max_attempts) || 3
  timeout = params.delete(:timeout) || 10

  current_params = params.dup
  responses = []

  loop do
    response = request_with_retry('account_tx', current_params, max_attempts: max_attempts, timeout: timeout)
    responses << response

    marker = response.dig('result', 'marker')
    break unless marker
    break if page_limit && responses.size >= page_limit

    current_params = current_params.merge(marker: marker)
  end

  responses
end

#account_tx_response(**params) ⇒ Object



221
222
223
# File 'lib/xrpl/client.rb', line 221

def (**params)
  request_with_retry('account_tx', params)
end

#autofill(transaction, signers_count: 0) ⇒ Hash

Fills in the fields a transaction needs before signing: Sequence, Fee and LastLedgerSequence. Existing values are never overwritten.

Parameters:

  • transaction (Hash)

    the (string-keyed) transaction to complete.

  • signers_count (Integer) (defaults to: 0)

    number of signatures for multisign fee scaling.

Returns:

  • (Hash)

    a copy of the transaction with the missing fields filled in.



312
313
314
315
316
317
318
# File 'lib/xrpl/client.rb', line 312

def autofill(transaction, signers_count: 0)
  tx = transaction.dup
  tx['Sequence'] ||= fetch_sequence(tx.fetch('Account'))
  tx['Fee'] ||= calculate_fee(signers_count)
  tx['LastLedgerSequence'] ||= current_ledger_index + LEDGER_OFFSET
  tx
end

#connect(wait: false, timeout: 10) ⇒ self

Opens the WebSocket connection.

By default this is non-blocking (preserving the previous behaviour) and returns self. Pass wait: true (or use #connect!) to block until the socket is actually open, so a following request can't race with connection setup and hit "Not connected".

Parameters:

  • wait (Boolean) (defaults to: false)

    block until the connection is open.

  • timeout (Numeric) (defaults to: 10)

    seconds to wait when wait is true.

Returns:

  • (self)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/xrpl/client.rb', line 61

def connect(wait: false, timeout: 10)
  @open = false
  @ready_queue = Queue.new

  Thread.new { EM.run } unless EM.reactor_running?

  EM.next_tick do
    @connection = Faye::WebSocket::Client.new(@url)

    @connection.on :open do |event|
      @open = true
      @ready_queue.push(:open)
      log("Connected to #{@url}")
    end

    @connection.on :message do |event|
      handle_message(JSON.parse(event.data))
    end

    @connection.on :error do |event|
      @ready_queue.push([:error, event.message])
    end

    @connection.on :close do |event|
      @open = false
      @connection = nil
      log("Connection closed: #{event.code} #{event.reason}")
    end
  end

  wait_until_open(timeout: timeout) if wait
  self
end

#connect!(timeout: 10) ⇒ self

Opens the connection and blocks until it is ready to accept requests.

Parameters:

  • timeout (Numeric) (defaults to: 10)

    seconds to wait for the socket to open.

Returns:

  • (self)


99
100
101
# File 'lib/xrpl/client.rb', line 99

def connect!(timeout: 10)
  connect(wait: true, timeout: timeout)
end

#disconnectObject



127
128
129
# File 'lib/xrpl/client.rb', line 127

def disconnect
  @connection&.close
end

#fee(**params) ⇒ Object



288
289
290
# File 'lib/xrpl/client.rb', line 288

def fee(**params)
  request('fee', **params)
end

#fee_response(**params) ⇒ Object



292
293
294
# File 'lib/xrpl/client.rb', line 292

def fee_response(**params)
  request_with_retry('fee', params)
end

#gateway_balances(**params) ⇒ Object



260
261
262
# File 'lib/xrpl/client.rb', line 260

def gateway_balances(**params)
  request('gateway_balances', **params)
end

#ledger(**params) ⇒ Object



268
269
270
# File 'lib/xrpl/client.rb', line 268

def ledger(**params)
  request('ledger', **params)
end

#ledger_closed(**params) ⇒ Object



272
273
274
# File 'lib/xrpl/client.rb', line 272

def ledger_closed(**params)
  request('ledger_closed', **params)
end

#ledger_current(**params) ⇒ Object



276
277
278
# File 'lib/xrpl/client.rb', line 276

def ledger_current(**params)
  request('ledger_current', **params)
end

#ledger_data(**params) ⇒ Object



280
281
282
# File 'lib/xrpl/client.rb', line 280

def ledger_data(**params)
  request('ledger_data', **params)
end

#ledger_entry(**params) ⇒ Object



284
285
286
# File 'lib/xrpl/client.rb', line 284

def ledger_entry(**params)
  request('ledger_entry', **params)
end

#noripple_check(**params) ⇒ Object



264
265
266
# File 'lib/xrpl/client.rb', line 264

def noripple_check(**params)
  request('noripple_check', **params)
end

#open?Boolean

Returns whether the WebSocket connection is currently open.

Returns:

  • (Boolean)

    whether the WebSocket connection is currently open.



104
105
106
# File 'lib/xrpl/client.rb', line 104

def open?
  @open
end

#request(command, params = {}) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/xrpl/client.rb', line 131

def request(command, params = {})
  id = SecureRandom.uuid
  register_pending_request(id)
  payload = {
    id: id,
    command: command
  }.merge(params)

  send_message(payload)
  # TODO: Implement promise/future or callback for response
  id
end

#request_with_response(command, params = {}, timeout: 10) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/xrpl/client.rb', line 144

def request_with_response(command, params = {}, timeout: 10)
  id = SecureRandom.uuid
  queue = Queue.new
  register_pending_request(id, queue: queue)

  payload = {
    id: id,
    command: command
  }.merge(params)

  send_message(payload)

  Timeout.timeout(timeout) { queue.pop }
rescue Timeout::Error
  @requests.delete(id)
  raise Timeout::Error, "Request timed out after #{timeout} seconds"
end

#request_with_retry(command, params = {}, max_attempts: 3, timeout: 10, retry_exceptions: [RuntimeError, Timeout::Error], **keyword_params) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/xrpl/client.rb', line 162

def request_with_retry(command, params = {}, max_attempts: 3, timeout: 10,
                       retry_exceptions: [RuntimeError, Timeout::Error], **keyword_params)
  attempt = 0
  request_params = keyword_params.empty? ? params : params.merge(keyword_params)

  begin
    attempt += 1
    request_with_response(command, request_params, timeout: timeout)
  rescue *retry_exceptions => error
    raise error if attempt >= max_attempts

    retry
  end
end

#submit(transaction, wallet:, autofill: true, fail_hard: false) ⇒ Hash

Autofills (optional), signs with the given wallet and submits a transaction.

Parameters:

  • transaction (Hash)

    the transaction to submit.

  • wallet (Wallet::Wallet)

    wallet used to sign.

  • autofill (Boolean) (defaults to: true)

    whether to autofill missing fields first.

  • fail_hard (Boolean) (defaults to: false)

    reject the transaction rather than queueing it.

Returns:

  • (Hash)

    the raw submit response.



327
328
329
330
# File 'lib/xrpl/client.rb', line 327

def submit(transaction, wallet:, autofill: true, fail_hard: false)
  prepared = prepare_for_submit(transaction, wallet: wallet, autofill: autofill)
  submit_blob(prepared[:tx_blob], fail_hard: fail_hard)
end

#submit_and_wait(transaction, wallet:, autofill: true, fail_hard: false, timeout: 20) ⇒ Hash

Like #submit, but then polls the ledger until the transaction is final (included in a validated ledger, or definitively failed/expired).

Parameters:

  • transaction (Hash)

    the transaction to submit.

  • wallet (Wallet::Wallet)

    wallet used to sign.

  • autofill (Boolean) (defaults to: true)

    whether to autofill missing fields first.

  • fail_hard (Boolean) (defaults to: false)

    reject the transaction rather than queueing it.

  • timeout (Numeric) (defaults to: 20)

    max seconds to wait for validation.

Returns:

  • (Hash)

    the validated tx response.

Raises:



342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/xrpl/client.rb', line 342

def submit_and_wait(transaction, wallet:, autofill: true, fail_hard: false, timeout: 20)
  prepared = prepare_for_submit(transaction, wallet: wallet, autofill: autofill)
  last_ledger = prepared[:tx]['LastLedgerSequence']
  unless last_ledger
    raise ArgumentError, 'Transaction must contain a LastLedgerSequence for reliable submission'
  end

  response = submit_blob(prepared[:tx_blob], fail_hard: fail_hard)
  preliminary = response.dig('result', 'engine_result')

  wait_for_final_outcome(prepared[:hash], last_ledger, preliminary, timeout: timeout)
end

#subscribe(**params) ⇒ Object



177
178
179
# File 'lib/xrpl/client.rb', line 177

def subscribe(**params)
  request('subscribe', **params)
end

#summarize_account_tx(response) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/xrpl/client.rb', line 247

def (response)
  result = response.fetch('result', {})
  transactions = Array(result['transactions'])

  {
    'ledger_index_min' => result['ledger_index_min'],
    'ledger_index_max' => result['ledger_index_max'],
    'transaction_count' => transactions.size,
    'validated' => result['validated'] == true,
    'marker_present' => !result['marker'].nil?
  }
end

#tx(**params) ⇒ Object



296
297
298
# File 'lib/xrpl/client.rb', line 296

def tx(**params)
  request('tx', **params)
end

#tx_response(**params) ⇒ Object



300
301
302
# File 'lib/xrpl/client.rb', line 300

def tx_response(**params)
  request_with_retry('tx', params)
end

#unsubscribe(**params) ⇒ Object



181
182
183
# File 'lib/xrpl/client.rb', line 181

def unsubscribe(**params)
  request('unsubscribe', **params)
end

#wait_until_open(timeout: 10) ⇒ true

Blocks the calling thread until the connection is open.

Parameters:

  • timeout (Numeric) (defaults to: 10)

    seconds to wait before giving up.

Returns:

  • (true)

    once the socket is open.

Raises:

  • (XRPL::ConnectionError)

    if the connection reports an error first.

  • (Timeout::Error)

    if the socket does not open within timeout.



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/xrpl/client.rb', line 114

def wait_until_open(timeout: 10)
  return true if @open

  signal = Timeout.timeout(timeout) { @ready_queue.pop }
  if signal.is_a?(Array) && signal.first == :error
    raise ConnectionError, "WebSocket connection failed: #{signal.last}"
  end

  true
rescue Timeout::Error
  raise Timeout::Error, "Connection did not open within #{timeout} seconds"
end