nwc-ruby 0.2.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97f270975d59347524e31943926149e2d361833a5ab9dd7c580cf00281b5fec9
4
+ data.tar.gz: 99b7a6b3a40c8c0f7ca30408f34afb653fab4c5afb8c69327e79be3a00afa663
5
+ SHA512:
6
+ metadata.gz: 96bc19c5c7ee1ac4794c76be6aa9e64b9aeddf7d89d5a4df88134104f07e65a40c1466efa54ffea9df546d55a92bb06c27e2ee1e8397d3c9459272bca6065526
7
+ data.tar.gz: 20f46cf66670993db5c8fa351e2bc95b611c7ad994dd7d64495927ffac5347c8b9a1a8767cb19dd16a2deb2dea869086ae8ae606a13211f55f3546156526afd8
data/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] — 2026-04-20
11
+
12
+ ### Changed
13
+
14
+ - **Breaking:** Split `NwcRuby.test` into two methods: `NwcRuby.test` (info +
15
+ read tests + write test if applicable) and `NwcRuby.test_notifications`
16
+ (subscribe and block forever, printing each notification). Notifications are
17
+ now tested in a separate process instead of a background thread, which
18
+ fixes the issue where notifications were never received during the
19
+ integrated test.
20
+ - Removed `test_send_and_receive` and `test_readonly` — a single `test` method
21
+ handles both read-only and read+write codes. If the code is read-only but
22
+ a Lightning address is provided, it prints a helpful warning instead of failing.
23
+ - `bin/nwc_test` now accepts an optional `notifications` subcommand:
24
+ `bin/nwc_test` (default test), `bin/nwc_test notifications` (listen forever).
25
+
26
+ ## [0.1.1] — 2026-04-20
27
+
28
+ ### Fixed
29
+
30
+ - `subscribe_to_notifications` crashed on startup — `RelayConnection#initialize` was missing
31
+ the `poll_interval:` keyword argument passed by the client.
32
+ - Ctrl+C / SIGINT / SIGTERM now exits cleanly — signal trap closes the WebSocket to unblock
33
+ the read loop instead of only setting a flag.
34
+
35
+ ### Changed
36
+
37
+ - `poll_interval` default changed from `5` to `nil` (disabled). The relay pushes events to
38
+ active subscriptions, so periodic re-subscribe polling is unnecessary. Pass
39
+ `poll_interval: 5` explicitly if your relay requires it.
40
+
41
+ ## [0.1.0] — 2026-04-20
42
+
43
+ ### Added
44
+
45
+ - Initial release.
46
+ - NIP-01 event construction, serialization, SHA-256 id, BIP-340 Schnorr signing & verification.
47
+ - NIP-04 (AES-256-CBC) encryption, for wallets that still only support legacy DMs.
48
+ - NIP-44 v2 encryption (ChaCha20 + HMAC-SHA256 + power-of-two padding), validated against
49
+ [paulmillr/nip44](https://github.com/paulmillr/nip44/blob/main/nip44.vectors.json).
50
+ - NIP-47 `nostr+walletconnect://` URI parser.
51
+ - Full NIP-47 method coverage: `pay_invoice`, `multi_pay_invoice`, `pay_keysend`,
52
+ `multi_pay_keysend`, `make_invoice`, `lookup_invoice`, `list_transactions`,
53
+ `get_balance`, `get_info`, `sign_message`.
54
+ - Notification listener (kinds 23196 and 23197) with dedupe by `payment_hash`.
55
+ - Reliable long-running `Transport::RelayConnection`: RFC 6455 ping (30 s),
56
+ pong deadline (45 s), forced recycle (5 min), capped exponential backoff,
57
+ SIGTERM/SIGINT handling.
58
+ - `NwcRuby.test` and `NwcRuby.test_notifications` diagnostic methods, backed
59
+ by `NwcRuby::TestRunner`. Announces read-only vs read+write, exercises every
60
+ advertised method, pays a Lightning address if the code is read+write.
61
+ Callable from IRB, Rails console, RSpec, or a rake task in the host app.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MegalithicBTC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,588 @@
1
+ # nwc-ruby
2
+
3
+ A production-grade Ruby client for [Nostr Wallet Connect (NIP-47)][nip47].
4
+
5
+ ```ruby
6
+ require "nwc_ruby"
7
+
8
+ client = NwcRuby::Client.from_uri(ENV["NWC_URL"])
9
+
10
+ # Create an invoice
11
+ invoice = client.make_invoice(amount: 1_000, description: "tip")
12
+ puts invoice["invoice"]
13
+
14
+ # Listen for payments, forever, reliably
15
+ client.subscribe_to_notifications do |n|
16
+ puts "Got paid: #{n.amount_msats} msats for #{n.payment_hash}"
17
+ end
18
+ ```
19
+
20
+ That's it. The gem handles the Nostr protocol, encryption, WebSocket lifecycle,
21
+ heartbeats, zombie-TCP detection, reconnects, and backoff. You call methods.
22
+
23
+ [nip47]: https://github.com/nostr-protocol/nips/blob/master/47.md
24
+
25
+ ---
26
+
27
+ ## Features
28
+
29
+ - **Full NIP-47 coverage** — `pay_invoice`, `multi_pay_invoice`, `pay_keysend`,
30
+ `multi_pay_keysend`, `make_invoice`, `lookup_invoice`, `list_transactions`,
31
+ `get_balance`, `get_info`, `sign_message`.
32
+ - **Notifications** — `payment_received` and `payment_sent` via kinds 23196
33
+ (NIP-04) and 23197 (NIP-44 v2), deduplicated by `payment_hash`.
34
+ - **Both encryption schemes** — NIP-44 v2 when the wallet advertises it
35
+ (validated against [paulmillr's test vectors][vectors]), NIP-04 fallback for
36
+ wallets that haven't migrated.
37
+ - **Bulletproof long-running transport** — 30 s ping, 45 s pong deadline, 5-min
38
+ forced recycle, capped exponential backoff, clean SIGTERM handling. Built on
39
+ [async-websocket][aw] (no dead EventMachine dependency).
40
+ - **Two diagnostic methods** — `NwcRuby.test` (info, read tests, write test
41
+ if applicable) and `NwcRuby.test_notifications` (listen forever in a separate
42
+ process). Each tells you whether your NWC code works, exercises the methods
43
+ the service advertises, and flags non-conforming responses with actionable
44
+ errors. Callable from IRB, a Rails console, a spec, or a rake task.
45
+
46
+ [vectors]: https://github.com/paulmillr/nip44/blob/main/nip44.vectors.json
47
+ [aw]: https://github.com/socketry/async-websocket
48
+
49
+ ---
50
+
51
+ ## Get a free NWC connection string
52
+
53
+ You need a `nostr+walletconnect://...` URI. Two free, reliable options:
54
+
55
+ - **[rizful.com][rizful]** — Lightning vaults and cloud-based Lightning nodes designed for reliability and NWC support.
56
+ built by the Megalith Node team. Dedicated NWC relay.
57
+ - **[getalby.com][alby]** — Alby Hub (self-hosted) or Alby Cloud. The
58
+ most widely used NWC implementation.
59
+
60
+ [rizful]: https://rizful.com
61
+ [alby]: https://getalby.com
62
+
63
+ ---
64
+
65
+ ## Read-only vs read+write — understand this before you build
66
+
67
+ This is the single most important concept to internalize before using NWC. A
68
+ connection string's capabilities are set **when the wallet issues it** and
69
+ cannot be widened by the client.
70
+
71
+ ### Read-only code
72
+
73
+ A read-only NWC code supports `get_info`, `get_balance`, `make_invoice`,
74
+ `lookup_invoice`, `list_transactions`, and typically notifications — but **not**
75
+ `pay_invoice` or `pay_keysend`. It cannot move funds out of the wallet.
76
+
77
+ **Use read-only for:** e-commerce checkouts (Shopify plugins, donation pages,
78
+ paywall integrations). Your server generates invoices, watches for
79
+ `payment_received` notifications, and credits the purchase. Even if your server
80
+ is fully compromised, the attacker cannot drain your wallet.
81
+
82
+ ### Read+write code
83
+
84
+ A read+write code adds `pay_invoice`, `multi_pay_invoice`, `pay_keysend`, and
85
+ `multi_pay_keysend`. Anyone holding it can spend from your wallet up to the
86
+ budget / rate limits the wallet enforces.
87
+
88
+ **Use read+write for:** tipping bots, treasury automation, nostr zap clients,
89
+ any app that legitimately needs to send Lightning payments. **Treat the
90
+ connection string like a private key.** Don't commit it. Rotate it if leaked.
91
+ Use per-app codes with per-app budgets — never reuse your main wallet's code.
92
+
93
+ The gem tells you which mode you have:
94
+
95
+ ```ruby
96
+ client = NwcRuby::Client.from_uri(ENV["NWC_URL"])
97
+ puts client.read_only? # => true or false
98
+ puts client.capabilities # => ["get_info", "get_balance", "make_invoice", ...]
99
+ ```
100
+
101
+ Or from IRB / a Rails console:
102
+
103
+ ```ruby
104
+ NwcRuby.test_readonly(nwc_url: ENV["NWC_URL"])
105
+ # ...
106
+ # ℹ This is a READ-ONLY code. It cannot move funds.
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Installation
112
+
113
+ Add to your Gemfile:
114
+
115
+ ```ruby
116
+ gem "nwc-ruby"
117
+ ```
118
+
119
+ Or install directly:
120
+
121
+ ```sh
122
+ gem install nwc-ruby
123
+ ```
124
+
125
+ The `rbsecp256k1` dependency is a C extension that bundles and compiles
126
+ `libsecp256k1` from source during `gem install`. You need a C toolchain and
127
+ a few libraries available **before** running `bundle install`.
128
+
129
+ **macOS:**
130
+
131
+ ```sh
132
+ brew install automake openssl libtool pkg-config gmp libffi
133
+ ```
134
+
135
+ **Ubuntu / Debian:**
136
+
137
+ ```sh
138
+ sudo apt-get update
139
+ sudo apt-get install -y build-essential automake pkg-config libtool \
140
+ libffi-dev libgmp-dev
141
+ ```
142
+
143
+ **Alpine:**
144
+
145
+ ```sh
146
+ apk add build-base automake autoconf libtool pkgconfig gmp-dev libffi-dev
147
+ ```
148
+
149
+ **Docker (Kamal / production):**
150
+
151
+ ```dockerfile
152
+ RUN apt-get update && apt-get install -y --no-install-recommends \
153
+ build-essential automake pkg-config libtool libffi-dev libgmp-dev \
154
+ && rm -rf /var/lib/apt/lists/*
155
+ ```
156
+
157
+ If you see `LoadError: cannot load such file -- secp256k1` at runtime, the
158
+ native extension wasn't compiled. Install the build dependencies above and
159
+ run `gem pristine rbsecp256k1` (or re-run `bundle install`) to rebuild it.
160
+
161
+ ---
162
+
163
+ ## Usage
164
+
165
+ ### One-shot requests
166
+
167
+ Each call transparently opens a WebSocket, sends the request, waits for the
168
+ response, and closes the connection.
169
+
170
+ ```ruby
171
+ client = NwcRuby::Client.from_uri(ENV["NWC_URL"])
172
+
173
+ info = client.get_info
174
+ # => {"alias"=>"my-node", "color"=>"#3399FF", "pubkey"=>"...",
175
+ # "network"=>"mainnet", "block_height"=>820_000, "block_hash"=>"...",
176
+ # "methods"=>[...], "notifications"=>[...]}
177
+
178
+ balance = client.get_balance["balance"] # msats
179
+
180
+ invoice = client.make_invoice(amount: 10_000, description: "coffee")
181
+ # => {"type"=>"incoming", "state"=>"pending",
182
+ # "invoice"=>"lnbc100n1p...", "payment_hash"=>"...",
183
+ # "amount"=>10_000, "created_at"=>1_730_000_000, ...}
184
+
185
+ status = client.lookup_invoice(payment_hash: invoice["payment_hash"])
186
+ # state: "pending" -> "settled"
187
+
188
+ # Only if read+write:
189
+ client.pay_invoice(invoice: "lnbc...")
190
+ # => {"preimage"=>"<64 hex chars>", "fees_paid"=>1234}
191
+ ```
192
+
193
+ ### Listening for notifications
194
+
195
+ This is the scenario the gem is most carefully engineered for: a long-running
196
+ process that needs to credit invoices the instant they're paid.
197
+
198
+ ```ruby
199
+ client = NwcRuby::Client.from_uri(ENV["NWC_URL"])
200
+
201
+ client.subscribe_to_notifications do |notification|
202
+ case notification.type
203
+ when "payment_received"
204
+ Invoice.find_by(payment_hash: notification.payment_hash)
205
+ &.mark_paid!(amount_msats: notification.amount_msats)
206
+ when "payment_sent"
207
+ Payout.find_by(payment_hash: notification.payment_hash)&.mark_settled!
208
+ end
209
+ end
210
+ # Blocks forever. SIGTERM / SIGINT cause a clean exit.
211
+ ```
212
+
213
+ Under the hood, this subscribes to both kind 23196 (NIP-04) and kind 23197
214
+ (NIP-44 v2), dedupes by `payment_hash`, sends a WebSocket ping every 30 seconds,
215
+ reconnects with exponential backoff if the pong deadline is missed, and
216
+ force-recycles the connection every 5 minutes as a belt-and-suspenders check
217
+ against middleboxes that silently drop stale TCP streams.
218
+
219
+ #### Resuming after a restart
220
+
221
+ Persist the `created_at` of the last notification you processed and pass it as
222
+ `since:` on restart to avoid replaying history:
223
+
224
+ ```ruby
225
+ since = AppState.get("nwc_last_seen") || Time.now.to_i
226
+
227
+ client.subscribe_to_notifications(since: since) do |n|
228
+ process(n)
229
+ AppState.set("nwc_last_seen", n.event.created_at)
230
+ end
231
+ ```
232
+
233
+ ### Using in a Rails app (Kamal deployment)
234
+
235
+ The Ruby NWC listener is best deployed as a [Kamal role][kamal-roles] — the
236
+ same app image as your web container, but with a different `cmd`. This is the
237
+ canonical 37signals pattern for a Sidekiq/Solid Queue/cron/listener process.
238
+ **Don't use a Kamal "accessory" for this** — accessories are for third-party
239
+ services (Postgres, Redis) and are not redeployed on `kamal deploy`.
240
+
241
+ ```yaml
242
+ # config/deploy.yml
243
+ service: myapp
244
+ image: ghcr.io/myorg/myapp
245
+
246
+ servers:
247
+ web:
248
+ hosts: [10.0.0.10]
249
+ nwc_listener:
250
+ hosts: [10.0.0.10]
251
+ cmd: "bundle exec rake nwc:listen_in_app"
252
+ options:
253
+ memory: 512m
254
+ ```
255
+
256
+ **Important: make your notification handler idempotent.** During deploys,
257
+ reconnects, or if you scale `nwc_listener` to multiple hosts, the same
258
+ `payment_received` notification can be delivered more than once. The gem
259
+ deduplicates within a single process lifetime, but across restarts or multiple
260
+ instances you must handle duplicates at the database level. Use a unique
261
+ constraint on `payment_hash` (or an `UPDATE ... WHERE state != 'paid'` guard)
262
+ so that processing the same notification twice is a harmless no-op — never
263
+ double-credit a payment.
264
+
265
+ Define the listener rake task in your app. Use Postgres `LISTEN/NOTIFY` or
266
+ GoodJob to communicate with the web container:
267
+
268
+ ```ruby
269
+ # lib/tasks/nwc.rake (in your Rails app)
270
+ namespace :nwc do
271
+ task listen_in_app: :environment do
272
+ client = NwcRuby::Client.from_uri(ENV["NWC_URL"])
273
+ since = AppState.find_or_create_by(key: "nwc_last_seen").value.to_i
274
+ since = Time.now.to_i if since.zero?
275
+
276
+ client.subscribe_to_notifications(since: since) do |n|
277
+ Invoice.transaction do
278
+ # Idempotent: only transitions pending → paid, ignores already-paid rows.
279
+ rows = Invoice.where(payment_hash: n.payment_hash, state: "pending")
280
+ .update_all(
281
+ state: "paid",
282
+ paid_amount_msats: n.amount_msats,
283
+ paid_at: Time.at(n.event.created_at)
284
+ )
285
+ AppState.where(key: "nwc_last_seen").update_all(value: n.event.created_at)
286
+ ActiveRecord::Base.connection.execute("NOTIFY nwc_invoice_paid") if rows > 0
287
+ end
288
+ end
289
+ end
290
+ end
291
+ ```
292
+
293
+ Docker's `--restart unless-stopped` (Kamal's default) plus the gem's internal
294
+ reconnect loop plus a SIGTERM trap gives you crash-only reliability without
295
+ systemd, foreman, or any process supervisor.
296
+
297
+ If you run the listener on multiple hosts, each instance receives the same
298
+ notifications independently. This is fine as long as the handler is idempotent
299
+ (as shown above). Running multiple instances gives you redundancy — if one
300
+ host goes down, the others keep listening — but they do not partition work.
301
+
302
+ [kamal-roles]: https://kamal-deploy.org/docs/configuration/roles/
303
+
304
+ ---
305
+
306
+ ## Testing against a real wallet
307
+
308
+ The gem ships two diagnostic methods. Call them from anywhere — IRB, a Rails
309
+ console, an RSpec test, or a rake task in your own app.
310
+
311
+ ### `NwcRuby.test` — info, read tests, write test
312
+
313
+ Parses the connection string, fetches info, runs all read tests (`get_info`,
314
+ `get_balance`, `list_transactions`, `make_invoice`, `lookup_invoice`). If the
315
+ code is read+write **and** you provide a Lightning address, it also sends a
316
+ real payment via `pay_invoice`. If you provide a Lightning address but the code
317
+ is read-only, it prints a helpful warning instead of failing.
318
+
319
+ ```ruby
320
+ NwcRuby.test(
321
+ nwc_url: ENV["NWC_URL"],
322
+ pay_to_lightning_address: "you@rizful.com", # optional — only used if code is read+write
323
+ pay_to_satoshis_amount: 10 # default: 100
324
+ )
325
+ # => true if all checks passed, false otherwise
326
+ ```
327
+
328
+ ### `NwcRuby.test_notifications` — listen for notifications
329
+
330
+ Subscribes to notifications and **blocks forever**, printing each one as it
331
+ arrives. Run this in a **separate terminal / process**. Ctrl-C to stop.
332
+
333
+ ```ruby
334
+ NwcRuby.test_notifications(nwc_url: ENV["NWC_URL"])
335
+ ```
336
+
337
+ ### Using `bin/nwc_test`
338
+
339
+ ```sh
340
+ # Test (read tests + write test if applicable)
341
+ NWC_URL="..." PAY_TO_LIGHTNING_ADDRESS=you@example.com bin/nwc_test
342
+
343
+ # Listen for notifications (blocks forever — run in a separate terminal)
344
+ NWC_URL="..." bin/nwc_test notifications
345
+ ```
346
+
347
+ ### Calling from a Rails console
348
+
349
+ ```sh
350
+ bin/rails c
351
+ ```
352
+
353
+ ```ruby
354
+ NwcRuby.test(nwc_url: ENV["NWC_URL"])
355
+ ```
356
+
357
+ ### Wrapping in your own rake tasks
358
+
359
+ ```ruby
360
+ # lib/tasks/nwc.rake (in your Rails app)
361
+ namespace :nwc do
362
+ desc "Run NWC diagnostic (read tests + write test if applicable)."
363
+ task test: :environment do
364
+ ok = NwcRuby.test(
365
+ nwc_url: ENV.fetch("NWC_URL"),
366
+ pay_to_lightning_address: ENV["PAY_TO_LIGHTNING_ADDRESS"],
367
+ pay_to_satoshis_amount: Integer(ENV.fetch("PAY_TO_SATOSHIS_AMOUNT", 100))
368
+ )
369
+ exit(ok ? 0 : 1)
370
+ end
371
+
372
+ desc "Listen for NWC notifications (blocks forever)."
373
+ task notifications: :environment do
374
+ NwcRuby.test_notifications(nwc_url: ENV.fetch("NWC_URL"))
375
+ end
376
+ end
377
+ ```
378
+
379
+ ### Sample output (`NwcRuby.test`)
380
+
381
+ ```
382
+ NWC Ruby diagnostic
383
+
384
+ ✓ Connection string parsed
385
+ ✓ Fetched info event (kind 13194)
386
+
387
+ ⚠ This code is READ+WRITE and can allow payments. Be careful with it.
388
+
389
+ Supported methods:
390
+ ✓ pay_invoice (mutating)
391
+ ✓ multi_pay_invoice (mutating)
392
+ — pay_keysend
393
+ — multi_pay_keysend
394
+ ✓ make_invoice
395
+ ✓ lookup_invoice
396
+ ✓ list_transactions
397
+ ✓ get_balance
398
+ ✓ get_info
399
+ — sign_message
400
+
401
+ Notifications: payment_received, payment_sent
402
+
403
+ ✓ Encryption: nip44_v2, nip04 — will use NIP-44 v2
404
+
405
+ Read tests
406
+ ✓ get_info (214ms)
407
+ ✓ get_balance (188ms)
408
+ ✓ list_transactions (312ms)
409
+ ✓ make_invoice (1000 msats) (267ms)
410
+ ✓ lookup_invoice (payment_hash from previous step) (241ms)
411
+
412
+ Write tests (read+write code detected, Lightning address provided)
413
+ ✓ pay_invoice (10 sats to you@rizful.com) (1843ms)
414
+
415
+ All tests passed.
416
+ ```
417
+
418
+ ### Sample output (`NwcRuby.test_notifications`)
419
+
420
+ ```
421
+ NWC Ruby diagnostic
422
+
423
+ ✓ Connection string parsed
424
+ ✓ Fetched info event (kind 13194)
425
+
426
+ Listening for notifications...
427
+ Press Ctrl-C to stop.
428
+
429
+ ✓ 2026-04-20 21:19:05 — payment_received
430
+ payment_hash=58e45fee1b5ebe83807944896ff99e9252594ef3be4e3404c3ba2ab4536a9988
431
+ amount=11000 msats
432
+ ```
433
+
434
+ When the wallet service misbehaves, the runner flags it:
435
+
436
+ ```
437
+ ✗ make_invoice: `type` should be 'incoming'
438
+ ✗ get_info: `network` is "unknown", expected one of mainnet/testnet/signet/regtest
439
+ ✗ lookup_invoice: wallet returned INTERNAL:
440
+ → The wallet service accepted the request but never responded.
441
+ ```
442
+
443
+ ---
444
+
445
+ ## API reference
446
+
447
+ ### `NwcRuby::Client`
448
+
449
+ Constructor:
450
+
451
+ | Method | Returns |
452
+ | ------------------------------- | --------------------------------------------------- |
453
+ | `Client.from_uri(uri)` | `Client` — parses the `nostr+walletconnect://` URI |
454
+ | `Client.new(connection_string)` | `Client` — if you already have a `ConnectionString` |
455
+
456
+ Introspection:
457
+
458
+ | Method | Returns |
459
+ | ----------------- | ---------------------------------------- |
460
+ | `#info(refresh:)` | `NIP47::Info` — cached on first call |
461
+ | `#capabilities` | `Array<String>` — supported method names |
462
+ | `#read_only?` | `Boolean` |
463
+ | `#read_write?` | `Boolean` |
464
+
465
+ Methods (all raise `WalletServiceError` on wallet-side errors and
466
+ `TimeoutError` after 30 s of silence):
467
+
468
+ | Method | Params | Returns (hash keys) |
469
+ | -------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
470
+ | `#pay_invoice` | `invoice:`, `amount:` (msats, optional partial) | `preimage`, `fees_paid` |
471
+ | `#multi_pay_invoice` | `invoices: [{id:, invoice:, amount:}]` | array of results |
472
+ | `#pay_keysend` | `amount:`, `pubkey:`, `preimage:`, `tlv_records:` | `preimage`, `fees_paid` |
473
+ | `#multi_pay_keysend` | `keysends: [...]` | array of results |
474
+ | `#make_invoice` | `amount:`, `description:`, `description_hash:`, `expiry:`, `metadata:` | `type`, `state`, `invoice`, `payment_hash`, `amount`, `created_at`, `expires_at` |
475
+ | `#lookup_invoice` | `payment_hash:` or `invoice:` | same as `make_invoice`, plus `settled_at`, `preimage` |
476
+ | `#list_transactions` | `from:`, `until_ts:`, `limit:`, `offset:`, `unpaid:`, `type:` | `transactions: [...]` |
477
+ | `#get_balance` | — | `balance` (msats) |
478
+ | `#get_info` | — | `alias`, `color`, `pubkey`, `network`, `block_height`, `block_hash`, `methods`, `notifications` |
479
+ | `#sign_message` | `message:` | `message`, `signature` |
480
+
481
+ Listener:
482
+
483
+ | Method | Description |
484
+ | ----------------------------------------------- | --------------------------------------------- |
485
+ | `#subscribe_to_notifications(since:) { \|n\| }` | Blocks forever. Yields `NIP47::Notification`. |
486
+
487
+ ### `NwcRuby::NIP47::Notification`
488
+
489
+ | Field | |
490
+ | --------------- | ---------------------------------------- |
491
+ | `#type` | `"payment_received"` or `"payment_sent"` |
492
+ | `#payment_hash` | hex |
493
+ | `#amount_msats` | integer |
494
+ | `#data` | full notification hash |
495
+ | `#event` | the underlying `Event` |
496
+
497
+ ### Errors
498
+
499
+ All gem errors inherit from `NwcRuby::Error`:
500
+
501
+ - `InvalidConnectionStringError` — the URI couldn't be parsed.
502
+ - `EncryptionError` — bad MAC / bad padding / unknown version byte / bad key.
503
+ - `InvalidSignatureError` — an event's signature did not verify.
504
+ - `TransportError` — the WebSocket couldn't connect or died unrecoverably.
505
+ - `TimeoutError` — no response within the timeout window.
506
+ - `UnsupportedMethodError` — wallet service doesn't advertise this method.
507
+ - `WalletServiceError` — the wallet returned an error envelope. Check `#code`
508
+ for `RATE_LIMITED`, `NOT_IMPLEMENTED`, `INSUFFICIENT_BALANCE`,
509
+ `QUOTA_EXCEEDED`, `RESTRICTED`, `UNAUTHORIZED`, `INTERNAL`,
510
+ `UNSUPPORTED_ENCRYPTION`, `PAYMENT_FAILED`, `NOT_FOUND`, or `OTHER`.
511
+
512
+ ---
513
+
514
+ ## Security notes
515
+
516
+ - **NIP-44 v2 correctness is a gated invariant.** The gem's test suite verifies
517
+ against [paulmillr's canonical vectors][vectors]. Report any discrepancy as
518
+ a security issue.
519
+ - **Never log the `secret` portion of a connection string.** It is a private
520
+ key. The gem's logger never emits it.
521
+ - **Prefer read-only codes** for any server that doesn't strictly need to
522
+ spend. The extra operational overhead of a read+write code (rotation, budget
523
+ limits, audit logging) is usually not worth it for checkout flows.
524
+ - **MAC verification runs in constant time before decryption returns.** The
525
+ `NIP44::Cipher.decrypt` path rejects unknown version bytes and fails closed
526
+ on bad padding.
527
+
528
+ ---
529
+
530
+ ## Development
531
+
532
+ ```sh
533
+ git clone https://github.com/MegalithicBTC/nwc-ruby
534
+ cd nwc-ruby
535
+ bundle install
536
+ bundle exec rspec
537
+ ```
538
+
539
+ To run the diagnostics against a real wallet while developing the gem itself:
540
+
541
+ ```sh
542
+ # Test (read + write if applicable)
543
+ bundle exec ruby -Ilib -rnwc_ruby -e '
544
+ NwcRuby.test(
545
+ nwc_url: ENV["NWC_URL"],
546
+ pay_to_lightning_address: ENV["LN_ADDR"],
547
+ pay_to_satoshis_amount: 10
548
+ )
549
+ '
550
+
551
+ # Listen for notifications (separate terminal)
552
+ bundle exec ruby -Ilib -rnwc_ruby -e '
553
+ NwcRuby.test_notifications(nwc_url: ENV["NWC_URL"])
554
+ '
555
+ ```
556
+
557
+ Or drop into IRB:
558
+
559
+ ```sh
560
+ bundle exec irb -Ilib -rnwc_ruby
561
+ > NwcRuby.test(nwc_url: ENV["NWC_URL"])
562
+ ```
563
+
564
+ ---
565
+
566
+ ## Contributing
567
+
568
+ PRs welcome. Please include RSpec coverage. For crypto changes, include or
569
+ update the vectors in `spec/fixtures/`.
570
+
571
+ 1. Fork it
572
+ 2. Create your feature branch (`git checkout -b feature/my-change`)
573
+ 3. Commit your changes
574
+ 4. Push to the branch
575
+ 5. Create a Pull Request
576
+
577
+ ## License
578
+
579
+ MIT. See [LICENSE](LICENSE).
580
+
581
+ ## Prior art and thanks
582
+
583
+ - [NIP-47 spec][nip47]
584
+ - [`@getalby/sdk`][alby-sdk] — the reference JavaScript implementation.
585
+ - [BLFS][blfs] — Bitcoin Lightning For Shopify
586
+
587
+ [alby-sdk]: https://github.com/getAlby/js-sdk/tree/master/src/nwc
588
+ [blfs]: https://docs.megalithic.me/BLFS/
data/lib/nwc-ruby.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'nwc_ruby'