Class: NwcRuby::Client

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

Overview

The main public API.

client = NwcRuby::Client.from_uri(ENV["NWC_URL"])

# One-shot request/response (transparently opens a WS, sends, waits, closes):
info    = client.get_info
balance = client.get_balance
invoice = client.make_invoice(amount: 1_000, description: "tip")

# Long-running listener (transparently heartbeats, reconnects, resumes):
client.subscribe_to_notifications do |notification|
  puts "Got #{notification.amount_msats} msats"
end

Constant Summary collapse

DEFAULT_TIMEOUT =
30

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_string, logger: nil, request_timeout: DEFAULT_TIMEOUT) ⇒ Client

Returns a new instance of Client.



30
31
32
33
34
35
# File 'lib/nwc_ruby/client.rb', line 30

def initialize(connection_string, logger: nil, request_timeout: DEFAULT_TIMEOUT)
  @connection_string = connection_string
  @logger            = logger || default_logger
  @request_timeout   = request_timeout
  @info              = nil
end

Instance Attribute Details

#connection_stringObject (readonly)

Returns the value of attribute connection_string.



24
25
26
# File 'lib/nwc_ruby/client.rb', line 24

def connection_string
  @connection_string
end

#loggerObject (readonly)

Returns the value of attribute logger.



24
25
26
# File 'lib/nwc_ruby/client.rb', line 24

def logger
  @logger
end

Class Method Details

.from_uri(uri_string) ⇒ Object



26
27
28
# File 'lib/nwc_ruby/client.rb', line 26

def self.from_uri(uri_string, **)
  new(ConnectionString.parse(uri_string), **)
end

Instance Method Details

#capabilitiesObject



47
48
49
# File 'lib/nwc_ruby/client.rb', line 47

def capabilities
  info.methods
end

#get_balanceObject



111
112
113
# File 'lib/nwc_ruby/client.rb', line 111

def get_balance
  call(NIP47::Methods::GET_BALANCE, {})
end

#get_infoObject



115
116
117
# File 'lib/nwc_ruby/client.rb', line 115

def get_info
  call(NIP47::Methods::GET_INFO, {})
end

#info(refresh: false) ⇒ Object

Fetch and cache the kind 13194 info event. This tells us which methods the wallet service supports and which encryption schemes it accepts.



41
42
43
44
45
# File 'lib/nwc_ruby/client.rb', line 41

def info(refresh: false)
  return @info if @info && !refresh

  @info = fetch_info
end

#list_transactions(from: nil, until_ts: nil, limit: nil, offset: nil, unpaid: nil, type: nil) ⇒ Object



100
101
102
103
104
105
106
107
108
109
# File 'lib/nwc_ruby/client.rb', line 100

def list_transactions(from: nil, until_ts: nil, limit: nil, offset: nil, unpaid: nil, type: nil)
  params = {}
  params['from']   = from       if from
  params['until']  = until_ts   if until_ts
  params['limit']  = limit      if limit
  params['offset'] = offset     if offset
  params['unpaid'] = unpaid unless unpaid.nil?
  params['type']   = type if type
  call(NIP47::Methods::LIST_TRANSACTIONS, params)
end

#lookup_invoice(payment_hash: nil, invoice: nil) ⇒ Object

Raises:

  • (ArgumentError)


91
92
93
94
95
96
97
98
# File 'lib/nwc_ruby/client.rb', line 91

def lookup_invoice(payment_hash: nil, invoice: nil)
  raise ArgumentError, 'lookup_invoice requires payment_hash or invoice' if payment_hash.nil? && invoice.nil?

  params = {}
  params['payment_hash'] = payment_hash if payment_hash
  params['invoice']      = invoice      if invoice
  call(NIP47::Methods::LOOKUP_INVOICE, params)
end

#make_invoice(amount:, description: nil, description_hash: nil, expiry: nil, metadata: nil) ⇒ Object



82
83
84
85
86
87
88
89
# File 'lib/nwc_ruby/client.rb', line 82

def make_invoice(amount:, description: nil, description_hash: nil, expiry: nil, metadata: nil)
  params = { 'amount' => amount }
  params['description']      = description      if description
  params['description_hash'] = description_hash if description_hash
  params['expiry']           = expiry           if expiry
  params['metadata']         =          if 
  call(NIP47::Methods::MAKE_INVOICE, params)
end

#multi_pay_invoice(invoices:) ⇒ Object



67
68
69
# File 'lib/nwc_ruby/client.rb', line 67

def multi_pay_invoice(invoices:)
  call(NIP47::Methods::MULTI_PAY_INVOICE, { 'invoices' => invoices })
end

#multi_pay_keysend(keysends:) ⇒ Object



78
79
80
# File 'lib/nwc_ruby/client.rb', line 78

def multi_pay_keysend(keysends:)
  call(NIP47::Methods::MULTI_PAY_KEYSEND, { 'keysends' => keysends })
end

#pay_invoice(invoice:, amount: nil) ⇒ Object

– NIP-47 methods ——————————————————-



61
62
63
64
65
# File 'lib/nwc_ruby/client.rb', line 61

def pay_invoice(invoice:, amount: nil)
  params = { 'invoice' => invoice }
  params['amount'] = amount if amount
  call(NIP47::Methods::PAY_INVOICE, params)
end

#pay_keysend(amount:, pubkey:, preimage: nil, tlv_records: nil) ⇒ Object



71
72
73
74
75
76
# File 'lib/nwc_ruby/client.rb', line 71

def pay_keysend(amount:, pubkey:, preimage: nil, tlv_records: nil)
  params = { 'amount' => amount, 'pubkey' => pubkey }
  params['preimage']    = preimage    if preimage
  params['tlv_records'] = tlv_records if tlv_records
  call(NIP47::Methods::PAY_KEYSEND, params)
end

#read_only?Boolean

Returns:

  • (Boolean)


51
52
53
# File 'lib/nwc_ruby/client.rb', line 51

def read_only?
  info.read_only?
end

#read_write?Boolean

Returns:

  • (Boolean)


55
56
57
# File 'lib/nwc_ruby/client.rb', line 55

def read_write?
  info.read_write?
end

#sign_message(message:) ⇒ Object



119
120
121
# File 'lib/nwc_ruby/client.rb', line 119

def sign_message(message:)
  call(NIP47::Methods::SIGN_MESSAGE, { 'message' => message })
end

#stop_notifications!Object

Stop a running subscribe_to_notifications listener.



124
125
126
# File 'lib/nwc_ruby/client.rb', line 124

def stop_notifications!
  @notification_connection&.stop!
end

#subscribe_to_notifications(since: Time.now.to_i, kinds: [NIP47::Methods::KIND_NOTIFICATION_NIP04, NIP47::Methods::KIND_NOTIFICATION_NIP44], sub_id: "nwc-#{SecureRandom.hex(4)}", poll_interval: nil, &block) ⇒ Object

Subscribe to payment_received / payment_sent notifications from the wallet service. Blocks forever, handling heartbeat and reconnect.

client.subscribe_to_notifications do |notification|
  case notification.type
  when "payment_received" then credit_invoice(notification.payment_hash, notification.amount_msats)
  when "payment_sent"     then mark_outbound_settled(notification.payment_hash)
  end
end

Parameters:

  • since (Integer) (defaults to: Time.now.to_i)

    unix timestamp for the ‘since:` filter on the subscription; defaults to now. Pass the last-seen `created_at` on reconnect to avoid replaying history.

  • kinds (Array<Integer>) (defaults to: [NIP47::Methods::KIND_NOTIFICATION_NIP04, NIP47::Methods::KIND_NOTIFICATION_NIP44])

    notification kinds to listen for. Defaults to both NIP-04 (23196) and NIP-44 v2 (23197). The listener dedupes by ‘payment_hash`, so receiving both is safe.

Raises:

  • (ArgumentError)


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/nwc_ruby/client.rb', line 146

def subscribe_to_notifications(since: Time.now.to_i, # rubocop:disable Metrics/MethodLength
                               kinds: [NIP47::Methods::KIND_NOTIFICATION_NIP04,
                                       NIP47::Methods::KIND_NOTIFICATION_NIP44],
                               sub_id: "nwc-#{SecureRandom.hex(4)}",
                               poll_interval: nil,
                               &block)
  raise ArgumentError, 'block required' unless block

  seen = {}
  last_seen_at = since
  filters = [{
    'authors' => [@connection_string.wallet_pubkey],
    '#p' => [@connection_string.client_pubkey],
    'kinds' => kinds,
    'since' => since
  }]

  conn = Transport::RelayConnection.new(
    url: @connection_string.relays.first,
    logger: @logger,
    poll_interval: poll_interval
  )
  @notification_connection = conn

  conn.on_open do |c|
    @logger.info(
      "[nwc] subscribing sub_id=#{sub_id} client_pubkey=#{@connection_string.client_pubkey} " \
      "wallet_pubkey=#{@connection_string.wallet_pubkey} kinds=#{kinds.inspect} since=#{since}"
    )
    c.send_req(sub_id: sub_id, filters: filters)
  end

  # Periodic re-subscribe: some relays (e.g. strfry) store ephemeral
  # events temporarily but never push them to existing subscriptions.
  # Re-sending the REQ with the same sub_id forces the relay to return
  # any newly-stored events.
  conn.on_poll do |c|
    filters[0]['since'] = last_seen_at
    @logger.debug("[nwc] re-subscribing sub_id=#{sub_id} since=#{last_seen_at}")
    c.send_req(sub_id: sub_id, filters: filters)
  end

  conn.on_event do |_sub, event_hash|
    event = Event.from_hash(event_hash)
    next unless event.valid_signature?
    next unless event.pubkey == @connection_string.wallet_pubkey

    # Advance the poll watermark so we don't re-fetch old events.
    last_seen_at = event.created_at if event.created_at && event.created_at > last_seen_at

    begin
      notification = NIP47::Notification.parse(event, @connection_string.secret, @connection_string.wallet_pubkey)
    rescue EncryptionError => e
      @logger.warn("[nwc] could not decrypt notification: #{e.message}")
      next
    end

    # Dedupe: wallets that support both encryption schemes publish both
    # 23196 and 23197 for the same event.
    key = notification.payment_hash || event.id
    next if seen[key]

    seen[key] = true
    # Primitive GC to keep the hash bounded.
    seen.shift while seen.size > 10_000

    block.call(notification)
  end

  conn.run!
end