Module: OpenReceive::Payments

Defined in:
lib/openreceive/core.rb

Overview

Ruby port of the JS core wallet-history walk (listIncomingTransactions in packages/js/core/src/payments.ts): pages list_transactions until every expected hash is seen, the wallet runs out of rows, or the page cap is reached. A walk that ended before the wallet ran out of rows is TRUNCATED — a hash such a walk did not see is unproven, never proven absent.

Class Method Summary collapse

Class Method Details

.list_incoming_transactions(client:, expected:, from: nil, until_time: nil, max_pages: nil, include_unpaid: false) ⇒ Object

client must respond to list_transactions(request) with an OpenReceive request hash; raw NIP-47 and already-normalized responses both work. Returns { rows: { "<payment_hash>" => }, truncated: true | false }.



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/openreceive/core.rb', line 482

def list_incoming_transactions(client:, expected:, from: nil, until_time: nil, max_pages: nil, include_unpaid: false)
  pages = normalize_max_pages(max_pages)
  outstanding = Array(expected).map { |value| normalize_payment_hash(value) }
  scan_from = from.nil? ? nil : normalize_unix(from, "from")
  scan_until = until_time.nil? ? nil : normalize_unix(until_time, "until")
  rows = {}
  offset = 0
  previous_page = nil
  # Proven false the moment the wallet runs out of rows or every expected
  # hash is accounted for; otherwise the walk hit its cap with rows still
  # to come.
  truncated = true
  pages.times do
    request = { "type" => "incoming", "limit" => TRANSACTION_PAGE_LIMIT, "offset" => offset }
    request["unpaid"] = true if include_unpaid
    request["from"] = scan_from unless scan_from.nil?
    request["until"] = scan_until unless scan_until.nil?
    page = Nwc.normalize_list_transactions_response(client.list_transactions(request)).fetch("transactions")
    page.each do |row|
      next unless row["type"].nil? || row["type"] == "incoming"
      payment_hash = row_payment_hash(row)
      next if payment_hash.nil?
      rows[payment_hash] = row
      outstanding.delete(payment_hash)
    end
    if outstanding.empty? || page.length < TRANSACTION_PAGE_LIMIT
      truncated = false
      break
    end
    # A wallet that ignores `offset` serves the same page forever; stop
    # instead of paging to the cap, and keep the scan marked incomplete.
    page_key = page.map { |row| row["payment_hash"].to_s }.join(",")
    break if page_key == previous_page
    previous_page = page_key
    offset += TRANSACTION_PAGE_LIMIT
  end
  { rows: rows, truncated: truncated }
end

.normalize_max_pages(value) ⇒ Object

Raises:

  • (ArgumentError)


521
522
523
524
525
526
# File 'lib/openreceive/core.rb', line 521

def normalize_max_pages(value)
  return 10_000 if value.nil?
  pages = Integer(value)
  raise ArgumentError, "max_pages must be a positive integer" unless pages.positive?
  pages
end

.normalize_payment_hash(value) ⇒ Object



528
529
530
531
532
533
534
# File 'lib/openreceive/core.rb', line 528

def normalize_payment_hash(value)
  normalized = value.to_s.strip.downcase
  unless /\A[0-9a-f]{64}\z/.match?(normalized)
    raise ArgumentError, "payment_hash must be 64 hexadecimal characters"
  end
  normalized
end

.normalize_unix(value, field) ⇒ Object

Raises:

  • (ArgumentError)


545
546
547
548
549
# File 'lib/openreceive/core.rb', line 545

def normalize_unix(value, field)
  number = Integer(value)
  raise ArgumentError, "#{field} must be a non-negative integer" if number.negative?
  number
end

.row_payment_hash(row) ⇒ Object

The scan key for one wallet row, or nil when the row can never match an attempt — a wallet-supplied row with a missing or malformed hash is skipped rather than rejected, so one quirky row cannot livelock reconciliation.



540
541
542
543
# File 'lib/openreceive/core.rb', line 540

def row_payment_hash(row)
  payment_hash = row["payment_hash"].to_s.strip.downcase
  /\A[0-9a-f]{64}\z/.match?(payment_hash) ? payment_hash : nil
end