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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, logger: nil, max_fee_drops: Fee::MAX_FEE_DROPS) ⇒ 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.

  • max_fee_drops (Integer) (defaults to: Fee::MAX_FEE_DROPS) —

    cap for the fee autofill computes, so a spike in the open ledger fee cannot burn an account. Transactions that cost an owner reserve (AccountDelete, AMMCreate, VaultCreate) are not capped.



59
60
61
62
63
64
65
66
67
# File 'lib/xrpl/client.rb', line 59

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

Instance Attribute Details

#connection ⇒ Object (readonly)

Returns the value of attribute connection.



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

def connection
  @connection
end

#url ⇒ Object (readonly)

Returns the value of attribute url.



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

def url
  @url
end

Class Method Details

.to_transaction_hash(transaction) ⇒ Hash, Object

Accepts a transaction as a plain Hash or as an XRPL::Transaction and hands back the Hash the rest of the pipeline works with. Anything else is passed through untouched, so a pre-signed blob still reaches submit.

Parameters:

Returns:

  • (Hash, Object)


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

def self.to_transaction_hash(transaction)
  return transaction if transaction.is_a?(Hash)
  return transaction.to_h if transaction.is_a?(XRPL::Transaction)

  transaction
end

Instance Method Details

#account_channels(**params) ⇒ Object



203
204
205
# File 'lib/xrpl/client.rb', line 203

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

#account_currencies(**params) ⇒ Object



207
208
209
# File 'lib/xrpl/client.rb', line 207

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

#account_info(**params) ⇒ Object



211
212
213
# File 'lib/xrpl/client.rb', line 211

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

#account_info_response(**params) ⇒ Object



215
216
217
# File 'lib/xrpl/client.rb', line 215

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

#account_lines(**params) ⇒ Object



219
220
221
# File 'lib/xrpl/client.rb', line 219

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

#account_nfts(**params) ⇒ Object



223
224
225
# File 'lib/xrpl/client.rb', line 223

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

#account_objects(**params) ⇒ Object



227
228
229
# File 'lib/xrpl/client.rb', line 227

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

#account_offers(**params) ⇒ Object



231
232
233
# File 'lib/xrpl/client.rb', line 231

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

#account_tx(**params) ⇒ Object



235
236
237
# File 'lib/xrpl/client.rb', line 235

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

#account_tx_all(**params) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/xrpl/client.rb', line 243

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



239
240
241
# File 'lib/xrpl/client.rb', line 239

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.

The fee follows the transaction type (see XRPL::Fee): an EscrowFinish pays for its Fulfillment, AccountDelete, AMMCreate and VaultCreate cost the owner reserve, a Batch pays for its inner transactions.

Parameters:

  • transaction (Hash, XRPL::Transaction) —

    the 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.



334
335
336
337
338
339
340
# File 'lib/xrpl/client.rb', line 334

def autofill(transaction, signers_count: 0)
  tx = self.class.to_transaction_hash(transaction).dup
  tx['Sequence'] ||= fetch_sequence(tx.fetch('Account'))
  tx['Fee'] ||= calculate_fee(tx, 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)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/xrpl/client.rb', line 79

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)


117
118
119
# File 'lib/xrpl/client.rb', line 117

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

#disconnect ⇒ Object



145
146
147
# File 'lib/xrpl/client.rb', line 145

def disconnect
  @connection&.close
end

#fee(**params) ⇒ Object



306
307
308
# File 'lib/xrpl/client.rb', line 306

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

#fee_response(**params) ⇒ Object



310
311
312
# File 'lib/xrpl/client.rb', line 310

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

#gateway_balances(**params) ⇒ Object



278
279
280
# File 'lib/xrpl/client.rb', line 278

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

#ledger(**params) ⇒ Object



286
287
288
# File 'lib/xrpl/client.rb', line 286

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

#ledger_closed(**params) ⇒ Object



290
291
292
# File 'lib/xrpl/client.rb', line 290

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

#ledger_current(**params) ⇒ Object



294
295
296
# File 'lib/xrpl/client.rb', line 294

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

#ledger_data(**params) ⇒ Object



298
299
300
# File 'lib/xrpl/client.rb', line 298

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

#ledger_entry(**params) ⇒ Object



302
303
304
# File 'lib/xrpl/client.rb', line 302

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

#noripple_check(**params) ⇒ Object



282
283
284
# File 'lib/xrpl/client.rb', line 282

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.



122
123
124
# File 'lib/xrpl/client.rb', line 122

def open?
  @open
end

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



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

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



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

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



180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/xrpl/client.rb', line 180

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.



349
350
351
352
# File 'lib/xrpl/client.rb', line 349

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:



364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/xrpl/client.rb', line 364

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



195
196
197
# File 'lib/xrpl/client.rb', line 195

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

#summarize_account_tx(response) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/xrpl/client.rb', line 265

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



314
315
316
# File 'lib/xrpl/client.rb', line 314

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

#tx_response(**params) ⇒ Object



318
319
320
# File 'lib/xrpl/client.rb', line 318

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

#unsubscribe(**params) ⇒ Object



199
200
201
# File 'lib/xrpl/client.rb', line 199

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.



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

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