openreceive-rails 0.2.1
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 +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +52 -0
- data/app/controllers/openreceive/application_controller.rb +117 -0
- data/app/controllers/openreceive/checkouts_controller.rb +21 -0
- data/app/controllers/openreceive/payments_controller.rb +37 -0
- data/app/controllers/openreceive/rates_controller.rb +18 -0
- data/app/controllers/openreceive/swaps_controller.rb +32 -0
- data/app/jobs/openreceive/reconcile_job.rb +15 -0
- data/app/models/open_receive_meta.rb +110 -0
- data/app/models/open_receive_payment.rb +337 -0
- data/config/routes.rb +15 -0
- data/lib/generators/openreceive/install/install_generator.rb +90 -0
- data/lib/generators/openreceive/install/templates/initializer.rb +94 -0
- data/lib/generators/openreceive/install/templates/migration.rb +80 -0
- data/lib/openreceive/configuration.rb +380 -0
- data/lib/openreceive/engine.rb +33 -0
- data/lib/openreceive/fulfillment_note.rb +30 -0
- data/lib/openreceive/generated/fulfillment_note.rb +60 -0
- data/lib/openreceive/rails/version.rb +10 -0
- data/lib/openreceive/rails.rb +37 -0
- data/lib/openreceive/reconcile.rb +316 -0
- data/lib/openreceive-rails.rb +5 -0
- data/lib/tasks/openreceive.rake +68 -0
- metadata +136 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openreceive/server"
|
|
5
|
+
|
|
6
|
+
module OpenReceive
|
|
7
|
+
# Floor for the durable reconcile-gate interval (seconds); stretched by
|
|
8
|
+
# invoice age (2s while any pending invoice is under 2 minutes old, 6s under
|
|
9
|
+
# 5 minutes, else 12s). Mirrors the JS OPENRECEIVE_MIN_RECONCILE_INTERVAL_SECONDS.
|
|
10
|
+
MIN_RECONCILE_INTERVAL_SECONDS = 2
|
|
11
|
+
# Wall-clock bound on an awaited request-path pass. Enforced as a deadline
|
|
12
|
+
# the wallet scan checks between page fetches rather than a Timeout.timeout:
|
|
13
|
+
# Thread#raise at an arbitrary point could tear down an ActiveRecord
|
|
14
|
+
# connection or kill a host's on_paid fulfillment mid-flight, on every
|
|
15
|
+
# winning request. A pass that runs out of budget simply stops walking.
|
|
16
|
+
RECONCILE_SCAN_TIMEOUT_SECONDS = 9
|
|
17
|
+
# Wallet-history pages a request-path pass may walk, mirroring the JS
|
|
18
|
+
# OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES.
|
|
19
|
+
RECONCILE_SCAN_MAX_PAGES = 50
|
|
20
|
+
# Cap on the `openreceive:notifications` worker's resubscribe backoff, and
|
|
21
|
+
# the subscription lifetime past which the ramp resets to 1s.
|
|
22
|
+
NOTIFICATIONS_MAX_BACKOFF_SECONDS = 60
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# One bounded reconciliation pass over the engine-owned payment ledger:
|
|
26
|
+
# scan the wallet for every pending attempt, deliver settlements through the
|
|
27
|
+
# settlement hook (write-once + on_paid), and persist terminal transitions
|
|
28
|
+
# so closed attempts leave the scan set. Attempt closure only ever happens
|
|
29
|
+
# from a successful wallet scan result observed at or after expiry plus
|
|
30
|
+
# OpenReceive::Server::Reconciliation::EXPIRY_GRACE_SECONDS — a local clock
|
|
31
|
+
# alone never closes a row, because a payment could have settled while the
|
|
32
|
+
# application was offline. A wallet failure raises and leaves every row
|
|
33
|
+
# pending for the next pass, and a hash absent from the pass results (a
|
|
34
|
+
# truncated scan never proved it absent) is no information — the attempt
|
|
35
|
+
# stays untouched.
|
|
36
|
+
#
|
|
37
|
+
# Runs on any OpenReceive call via maybe_reconcile! (default), from the
|
|
38
|
+
# optional `bin/rails openreceive:notifications` worker, or one-shot from
|
|
39
|
+
# OpenReceive::ReconcileJob / `bin/rails openreceive:reconcile`.
|
|
40
|
+
# Returns the per-hash check results of the pass (an array of
|
|
41
|
+
# { "payment_hash", "status", "paid_at"?, "details"? } hashes) so callers —
|
|
42
|
+
# notably payments/check — can serve a requested hash straight from the
|
|
43
|
+
# pass instead of adding a second per-invoice wallet walk.
|
|
44
|
+
def reconcile!(overlap_seconds: 60, now: nil, max_pages: nil, deadline: nil)
|
|
45
|
+
attempts = OpenReceivePayment.reconcilable_attempts
|
|
46
|
+
return [] if attempts.empty?
|
|
47
|
+
|
|
48
|
+
observed_at = Integer(now || Time.now.to_i)
|
|
49
|
+
request = {
|
|
50
|
+
"attempts" => attempts,
|
|
51
|
+
"overlap_seconds" => overlap_seconds,
|
|
52
|
+
"until" => observed_at + overlap_seconds
|
|
53
|
+
}
|
|
54
|
+
request["max_pages"] = max_pages unless max_pages.nil?
|
|
55
|
+
request["deadline"] = deadline unless deadline.nil?
|
|
56
|
+
results = config.service.reconcile_payments(request)
|
|
57
|
+
log_reconcile_pass(attempts, results, overlap_seconds, observed_at)
|
|
58
|
+
by_hash = attempts.to_h { |attempt| [attempt.fetch("payment_hash"), attempt] }
|
|
59
|
+
results.each do |checked|
|
|
60
|
+
attempt = by_hash[checked.fetch("payment_hash")]
|
|
61
|
+
next if attempt.nil?
|
|
62
|
+
|
|
63
|
+
if checked["status"] == "settled" && checked["paid_at"]
|
|
64
|
+
settle_attempt(checked)
|
|
65
|
+
else
|
|
66
|
+
record_attempt_transition(attempt, checked, observed_at)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
results
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Opportunistic settlement discovery, piggybacked on any OpenReceive call
|
|
73
|
+
# (the engine's around_action runs it before every mounted route): skip
|
|
74
|
+
# without a wallet call when nothing is pending, try the durable
|
|
75
|
+
# openreceive_meta gate shared by every Puma worker ("gate_busy" means
|
|
76
|
+
# another worker just scanned — skip the wallet), otherwise AWAIT one
|
|
77
|
+
# bounded reconcile! pass and return its per-hash results. Never raises: a
|
|
78
|
+
# failed or timed-out scan warns and returns "scan_failed" — the caller's
|
|
79
|
+
# own request must not fail because a settlement sweep did, and claimed_at
|
|
80
|
+
# stays in place so a broken wallet cannot stampede.
|
|
81
|
+
#
|
|
82
|
+
# Returns { "reason" => "ran", "checks" => [...] } or
|
|
83
|
+
# { "reason" => "disabled" | "no_pending" | "gate_busy" | "scan_failed" }.
|
|
84
|
+
# Exported for host code too: host-only routes (e.g. an app's own POST
|
|
85
|
+
# /orders) never auto-run it, but may call OpenReceive.maybe_reconcile!.
|
|
86
|
+
def maybe_reconcile!(now: nil)
|
|
87
|
+
setting = config.opportunistic_reconcile
|
|
88
|
+
return { "reason" => "disabled" } if setting == false
|
|
89
|
+
|
|
90
|
+
attempts = OpenReceivePayment.reconcilable_attempts
|
|
91
|
+
return { "reason" => "no_pending" } if attempts.empty?
|
|
92
|
+
|
|
93
|
+
observed_at = Integer(now || Time.now.to_i)
|
|
94
|
+
interval = reconcile_gate_interval_seconds(attempts, observed_at, setting)
|
|
95
|
+
unless OpenReceiveMeta.claim_reconcile_gate(now: observed_at, interval_seconds: interval)
|
|
96
|
+
# Another worker scanned within the interval; this request pays nothing.
|
|
97
|
+
openreceive_logger&.debug(
|
|
98
|
+
"[openreceive] opportunistic reconcile: gate_busy " \
|
|
99
|
+
"(#{attempts.length} pending, interval #{interval}s)"
|
|
100
|
+
)
|
|
101
|
+
return { "reason" => "gate_busy" }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
checks = reconcile!(
|
|
105
|
+
now: observed_at,
|
|
106
|
+
max_pages: RECONCILE_SCAN_MAX_PAGES,
|
|
107
|
+
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + RECONCILE_SCAN_TIMEOUT_SECONDS
|
|
108
|
+
)
|
|
109
|
+
{ "reason" => "ran", "checks" => checks }
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
openreceive_logger&.warn(
|
|
112
|
+
"[openreceive] opportunistic reconcile failed (will retry): #{sanitize_failure_message(e)}"
|
|
113
|
+
)
|
|
114
|
+
{ "reason" => "scan_failed" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Opt-in NWC-02 notifications: subscribe to the configured NWC client's
|
|
118
|
+
# `payment_received` notifications. Notifications are authenticated wallet
|
|
119
|
+
# data — a payload that satisfies the shared settlement rule (`settled_at`
|
|
120
|
+
# or a settled transaction state; never a preimage alone) and matches a
|
|
121
|
+
# pending attempt settles that attempt directly through the engine's
|
|
122
|
+
# write-once settlement path (mark_paid_once! + on_paid), with no redundant
|
|
123
|
+
# wallet scan for that invoice. Anything less — no finality signal, an
|
|
124
|
+
# unknown hash, or a direct-settlement failure — falls back to one bounded
|
|
125
|
+
# OpenReceive.reconcile! pass. Polling (OpenReceive::ReconcileJob /
|
|
126
|
+
# `bin/rails openreceive:reconcile`) remains the safety net for
|
|
127
|
+
# notifications missed while offline. Direct settlement assumes the NWC
|
|
128
|
+
# client binds notification decryption to the connection's wallet pubkey;
|
|
129
|
+
# a client that skips author verification must not be granted it.
|
|
130
|
+
#
|
|
131
|
+
# The client contract is one method, `subscribe_notifications(&handler)`,
|
|
132
|
+
# yielding NWC-02 wire payloads (`notification_type` plus the
|
|
133
|
+
# transaction-shaped `notification`) — the shape NwcRubyReceiveClient
|
|
134
|
+
# adapts nwc-ruby's notification object to. The handler filters
|
|
135
|
+
# `payment_received` itself, like the Node listener: an NWC-02
|
|
136
|
+
# subscription is not type-filtered, the wallet decides what it publishes.
|
|
137
|
+
# Returns whatever the client's subscribe call returns; blocking clients
|
|
138
|
+
# simply do not return until the subscription ends. Raises
|
|
139
|
+
# OpenReceive::ConfigurationError when the client does not support
|
|
140
|
+
# notifications.
|
|
141
|
+
def listen_for_notifications!(overlap_seconds: 60)
|
|
142
|
+
client = config.send(:resolved_nwc_client)
|
|
143
|
+
unless client.respond_to?(:subscribe_notifications)
|
|
144
|
+
raise ConfigurationError,
|
|
145
|
+
"The configured NWC client does not support NWC-02 notifications " \
|
|
146
|
+
"(no subscribe_notifications method). Notifications are optional; " \
|
|
147
|
+
"keep polling with OpenReceive::ReconcileJob or " \
|
|
148
|
+
"`bin/rails openreceive:reconcile`."
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
client.subscribe_notifications do |notification|
|
|
152
|
+
next unless payment_received_notification?(notification)
|
|
153
|
+
|
|
154
|
+
reconcile!(overlap_seconds: overlap_seconds) unless settle_from_notification!(notification)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Retry delay for the `openreceive:notifications` worker's subscribe loop:
|
|
159
|
+
# doubles per consecutive failure up to NOTIFICATIONS_MAX_BACKOFF_SECONDS,
|
|
160
|
+
# and a subscription that stayed up at least that long was healthy, so the
|
|
161
|
+
# next drop starts the ramp from scratch (mirrors the JS notifications
|
|
162
|
+
# worker, which reconnects fresh per subscription).
|
|
163
|
+
def notifications_retry_delay(previous_delay, subscribed_seconds)
|
|
164
|
+
return 1 if previous_delay.nil? || subscribed_seconds >= NOTIFICATIONS_MAX_BACKOFF_SECONDS
|
|
165
|
+
|
|
166
|
+
[previous_delay * 2, NOTIFICATIONS_MAX_BACKOFF_SECONDS].min
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Failure text can embed wallet credentials (an NWC URI inside a connect
|
|
170
|
+
# error); redact them before the message reaches the host log, mirroring
|
|
171
|
+
# the JS redactSecrets URI patterns. Public because the long-lived
|
|
172
|
+
# `openreceive:notifications` worker — the process most likely to see a
|
|
173
|
+
# connect error — logs failures of its own.
|
|
174
|
+
def sanitize_failure_message(error)
|
|
175
|
+
"#{error.class}: #{error.message}"
|
|
176
|
+
.gsub(/nostr\+walletconnect:[^\s"'`<>]+/, "[REDACTED_NWC]")
|
|
177
|
+
.gsub(/lightning\+swapconnect:[^\s"'`<>]+/, "[REDACTED_LSC]")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Rails.logger when the engine runs inside Rails; nil in bare-gem tests.
|
|
183
|
+
# Settlement behavior never depends on logging.
|
|
184
|
+
def openreceive_logger
|
|
185
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
186
|
+
|
|
187
|
+
::Rails.logger
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# One failing settlement (a raising on_paid, a host data problem)
|
|
191
|
+
# must not abort the rest of the pass: every later attempt would
|
|
192
|
+
# otherwise never settle and never close, on every pass.
|
|
193
|
+
def settle_attempt(checked)
|
|
194
|
+
config.settlement_hook.call(
|
|
195
|
+
"payment_hash" => checked.fetch("payment_hash"),
|
|
196
|
+
"paid_at" => checked.fetch("paid_at"),
|
|
197
|
+
"details" => checked["details"]
|
|
198
|
+
)
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
openreceive_logger&.warn(
|
|
201
|
+
"[openreceive] settlement for #{checked.fetch('payment_hash')} failed " \
|
|
202
|
+
"(will retry next pass): #{sanitize_failure_message(e)}"
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Closure is decided by the shared reconciliation rules from a scan result
|
|
207
|
+
# the wallet actually returned; a nil transition means keep waiting.
|
|
208
|
+
def record_attempt_transition(attempt, checked, observed_at)
|
|
209
|
+
wallet_transaction = checked.dig("details", "transaction") || {}
|
|
210
|
+
transition = OpenReceive::Server::Reconciliation.transition(
|
|
211
|
+
expires_at: attempt.fetch("expires_at"),
|
|
212
|
+
status: checked.fetch("status"),
|
|
213
|
+
observed_at: observed_at,
|
|
214
|
+
# The row here is the service's NORMALIZED output, which carries
|
|
215
|
+
# "transaction_state" only — the raw wallet's "state" spelling was
|
|
216
|
+
# already resolved at the client boundary.
|
|
217
|
+
transaction_state: wallet_transaction["transaction_state"]
|
|
218
|
+
)
|
|
219
|
+
return if transition.nil?
|
|
220
|
+
|
|
221
|
+
OpenReceivePayment.record_reconciliation!(
|
|
222
|
+
payment_hash: checked.fetch("payment_hash"),
|
|
223
|
+
status: transition.fetch("status"),
|
|
224
|
+
observed_at: observed_at,
|
|
225
|
+
reason: transition.fetch("reason")
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Info, not debug: passes are durably gated (min 2s apart, and only while
|
|
230
|
+
# attempts are pending), so operators can watch settlement discovery and
|
|
231
|
+
# the batched list_transactions window without raising the log level. All
|
|
232
|
+
# pending attempts share one creation-time window walked at most twice —
|
|
233
|
+
# never one wallet call per invoice.
|
|
234
|
+
def log_reconcile_pass(attempts, results, overlap_seconds, observed_at)
|
|
235
|
+
logger = openreceive_logger
|
|
236
|
+
return if logger.nil?
|
|
237
|
+
|
|
238
|
+
counts = results.group_by { |checked| checked["status"] }.transform_values(&:length)
|
|
239
|
+
window_from = [attempts.map { |attempt| Integer(attempt.fetch("created_at")) }.min - overlap_seconds, 0].max
|
|
240
|
+
logger.info(
|
|
241
|
+
"[openreceive] reconcile pass: #{attempts.length} pending attempt(s) in one " \
|
|
242
|
+
"batched list_transactions window (from #{window_from} until #{observed_at + overlap_seconds}, <=2 walks): " \
|
|
243
|
+
"#{counts.map { |status, count| "#{count} #{status}" }.join(', ')}"
|
|
244
|
+
)
|
|
245
|
+
rescue StandardError
|
|
246
|
+
# Diagnostics must never affect the pass.
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The gate interval for the current pending set: the configured floor
|
|
251
|
+
# (config.opportunistic_reconcile min_interval_seconds), stretched by
|
|
252
|
+
# invoice age — 2s while any pending invoice is under 2 minutes old, 6s
|
|
253
|
+
# under 5 minutes, else 12s. Mirrors the JS reconcile gate.
|
|
254
|
+
def reconcile_gate_interval_seconds(attempts, now, setting)
|
|
255
|
+
floor = MIN_RECONCILE_INTERVAL_SECONDS
|
|
256
|
+
if setting.is_a?(Hash)
|
|
257
|
+
configured = setting[:min_interval_seconds] || setting["min_interval_seconds"]
|
|
258
|
+
floor = [Integer(configured), floor].max unless configured.nil?
|
|
259
|
+
end
|
|
260
|
+
age_stretch = attempts.map do |attempt|
|
|
261
|
+
elapsed = [now - Integer(attempt.fetch("created_at")), 0].max
|
|
262
|
+
if elapsed < 120
|
|
263
|
+
2
|
|
264
|
+
elsif elapsed < 300
|
|
265
|
+
6
|
|
266
|
+
else
|
|
267
|
+
12
|
|
268
|
+
end
|
|
269
|
+
end.min
|
|
270
|
+
[floor, age_stretch].max
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def payment_received_notification?(notification)
|
|
274
|
+
return false unless notification.respond_to?(:[])
|
|
275
|
+
|
|
276
|
+
type = notification["notification_type"] || notification[:notification_type] ||
|
|
277
|
+
notification["type"] || notification[:type]
|
|
278
|
+
type.to_s == "payment_received"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Direct settlement from one authenticated payment_received payload.
|
|
282
|
+
# Returns true only when the payload, normalized like a list_transactions
|
|
283
|
+
# row, satisfies the shared settlement rule AND matches a pending attempt —
|
|
284
|
+
# in that case it settles through the engine's write-once settlement hook
|
|
285
|
+
# and no wallet scan runs for that invoice. Any other outcome (no payload,
|
|
286
|
+
# no finality signal, unknown/not-pending hash, or a failure) returns
|
|
287
|
+
# false so the caller falls back to a bounded reconciliation scan.
|
|
288
|
+
def settle_from_notification!(notification)
|
|
289
|
+
payload = notification["notification"] || notification[:notification]
|
|
290
|
+
return false unless payload.respond_to?(:each_pair)
|
|
291
|
+
|
|
292
|
+
transaction = OpenReceive::Nwc.normalize_transaction(payload)
|
|
293
|
+
return false unless OpenReceive::Settlement.status(transaction) == "settled"
|
|
294
|
+
|
|
295
|
+
payment_hash = transaction["payment_hash"].to_s.downcase
|
|
296
|
+
return false if payment_hash.empty?
|
|
297
|
+
|
|
298
|
+
return false unless OpenReceivePayment.pending.where(payment_hash: payment_hash).exists?
|
|
299
|
+
|
|
300
|
+
observed_at = Time.now.to_i
|
|
301
|
+
config.settlement_hook.call(
|
|
302
|
+
"payment_hash" => payment_hash,
|
|
303
|
+
"paid_at" => transaction["settled_at"] || observed_at,
|
|
304
|
+
"details" => {
|
|
305
|
+
"transaction" => transaction,
|
|
306
|
+
"observed_at" => observed_at,
|
|
307
|
+
"paid_at_source" => transaction["settled_at"] ? "settled_at" : "observed_at"
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
true
|
|
311
|
+
rescue StandardError
|
|
312
|
+
# A direct-settlement failure falls back to the scan-based safety net.
|
|
313
|
+
false
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :openreceive do
|
|
4
|
+
desc "Run one OpenReceive reconciliation pass over pending payment attempts"
|
|
5
|
+
task reconcile: :environment do
|
|
6
|
+
checks = OpenReceive.reconcile!
|
|
7
|
+
puts "openreceive:reconcile checked #{checks.length} pending attempt(s)"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The one optional worker (case 2). By default no worker is needed at all:
|
|
11
|
+
# every engine request runs the durably gated opportunistic reconcile, so
|
|
12
|
+
# settlement of abandoned checkouts piggybacks on ordinary traffic. Run this
|
|
13
|
+
# long-lived process only when you want settlements pushed the moment the
|
|
14
|
+
# wallet reports payment_received — it both listens for NWC-02 notifications
|
|
15
|
+
# AND runs a periodic reconcile pass in the same process, the safety net for
|
|
16
|
+
# notifications missed while this worker was down. Hosts do not additionally
|
|
17
|
+
# schedule OpenReceive::ReconcileJob; it remains available as a one-shot
|
|
18
|
+
# primitive.
|
|
19
|
+
desc "Optional worker: listen for NWC-02 notifications and reconcile periodically (long-running)"
|
|
20
|
+
task notifications: :environment do
|
|
21
|
+
interval = Integer(ENV.fetch("OPENRECEIVE_NOTIFICATIONS_RECONCILE_INTERVAL_SECONDS", "15"))
|
|
22
|
+
puts "openreceive:notifications listening for NWC-02 payment_received and " \
|
|
23
|
+
"reconciling every #{interval}s. Notifications are authenticated wallet data; " \
|
|
24
|
+
"the periodic pass covers notifications missed while this worker was down."
|
|
25
|
+
|
|
26
|
+
reconciler = Thread.new do
|
|
27
|
+
loop do
|
|
28
|
+
begin
|
|
29
|
+
OpenReceive.reconcile!
|
|
30
|
+
rescue StandardError => error
|
|
31
|
+
# Redacted: a connect failure can quote the NWC URI, secret and all.
|
|
32
|
+
warn "openreceive:notifications periodic reconcile failed (will retry): " \
|
|
33
|
+
"#{OpenReceive.sanitize_failure_message(error)}"
|
|
34
|
+
end
|
|
35
|
+
sleep interval
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
reconciler.abort_on_exception = false
|
|
39
|
+
|
|
40
|
+
backoff = nil
|
|
41
|
+
loop do
|
|
42
|
+
subscribed_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
43
|
+
failure = nil
|
|
44
|
+
begin
|
|
45
|
+
OpenReceive.listen_for_notifications!
|
|
46
|
+
rescue OpenReceive::ConfigurationError
|
|
47
|
+
# The configured NWC client cannot notify; surface the limitation
|
|
48
|
+
# clearly. The periodic pass alone would be a silent downgrade the
|
|
49
|
+
# operator asked this worker to exceed.
|
|
50
|
+
reconciler.kill
|
|
51
|
+
raise
|
|
52
|
+
rescue StandardError => error
|
|
53
|
+
failure = error
|
|
54
|
+
end
|
|
55
|
+
backoff = OpenReceive.notifications_retry_delay(
|
|
56
|
+
backoff, Process.clock_gettime(Process::CLOCK_MONOTONIC) - subscribed_at
|
|
57
|
+
)
|
|
58
|
+
if failure.nil?
|
|
59
|
+
warn "openreceive:notifications subscription ended; retrying in #{backoff}s"
|
|
60
|
+
else
|
|
61
|
+
warn "openreceive:notifications error: " \
|
|
62
|
+
"#{OpenReceive.sanitize_failure_message(failure)}; retrying in #{backoff}s " \
|
|
63
|
+
"(the periodic reconcile pass still covers settlements)"
|
|
64
|
+
end
|
|
65
|
+
sleep backoff
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: openreceive-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OpenReceive
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: openreceive
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - '='
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.2.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - '='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.2.1
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: openreceive-server
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.1
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.2.1
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '8.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '8.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: sqlite3
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.1'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.1'
|
|
68
|
+
description: 'A mountable Rails engine that ships OpenReceive''s receive-only checkout
|
|
69
|
+
routes into a Rails app. Engine controllers inherit from the host''s ApplicationController
|
|
70
|
+
(keeping its authentication, current_user, and forgery protection; the shared handler
|
|
71
|
+
adds JSON-only and same-site gates), delegate to the openreceive-server Service,
|
|
72
|
+
and obey host-supplied authorization, amount-resolution, and settlement hooks. The
|
|
73
|
+
engine owns the OpenReceivePayment attempt model, its status state machine, settlement
|
|
74
|
+
write-once, and reconciliation (OpenReceive.reconcile!, OpenReceive::ReconcileJob,
|
|
75
|
+
rake openreceive:reconcile); its install generator emits only the migration, initializer,
|
|
76
|
+
and route mount. Receive-only: it never exposes a spend path, and boot fails closed
|
|
77
|
+
on spend-capable NWC codes.'
|
|
78
|
+
email:
|
|
79
|
+
- info@openreceive.org
|
|
80
|
+
executables: []
|
|
81
|
+
extensions: []
|
|
82
|
+
extra_rdoc_files: []
|
|
83
|
+
files:
|
|
84
|
+
- CHANGELOG.md
|
|
85
|
+
- LICENSE
|
|
86
|
+
- README.md
|
|
87
|
+
- app/controllers/openreceive/application_controller.rb
|
|
88
|
+
- app/controllers/openreceive/checkouts_controller.rb
|
|
89
|
+
- app/controllers/openreceive/payments_controller.rb
|
|
90
|
+
- app/controllers/openreceive/rates_controller.rb
|
|
91
|
+
- app/controllers/openreceive/swaps_controller.rb
|
|
92
|
+
- app/jobs/openreceive/reconcile_job.rb
|
|
93
|
+
- app/models/open_receive_meta.rb
|
|
94
|
+
- app/models/open_receive_payment.rb
|
|
95
|
+
- config/routes.rb
|
|
96
|
+
- lib/generators/openreceive/install/install_generator.rb
|
|
97
|
+
- lib/generators/openreceive/install/templates/initializer.rb
|
|
98
|
+
- lib/generators/openreceive/install/templates/migration.rb
|
|
99
|
+
- lib/openreceive-rails.rb
|
|
100
|
+
- lib/openreceive/configuration.rb
|
|
101
|
+
- lib/openreceive/engine.rb
|
|
102
|
+
- lib/openreceive/fulfillment_note.rb
|
|
103
|
+
- lib/openreceive/generated/fulfillment_note.rb
|
|
104
|
+
- lib/openreceive/rails.rb
|
|
105
|
+
- lib/openreceive/rails/version.rb
|
|
106
|
+
- lib/openreceive/reconcile.rb
|
|
107
|
+
- lib/tasks/openreceive.rake
|
|
108
|
+
homepage: https://openreceive.org
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://openreceive.org
|
|
113
|
+
source_code_uri: https://github.com/openreceive/openreceive
|
|
114
|
+
changelog_uri: https://github.com/openreceive/openreceive/blob/master/packages/ruby/openreceive-rails/CHANGELOG.md
|
|
115
|
+
bug_tracker_uri: https://github.com/openreceive/openreceive/issues
|
|
116
|
+
documentation_uri: https://rubydoc.info/gems/openreceive-rails
|
|
117
|
+
rubygems_mfa_required: 'true'
|
|
118
|
+
rdoc_options: []
|
|
119
|
+
require_paths:
|
|
120
|
+
- lib
|
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '3.2'
|
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
requirements: []
|
|
132
|
+
rubygems_version: 3.6.8
|
|
133
|
+
specification_version: 4
|
|
134
|
+
summary: OpenReceive mountable Rails engine with engine-owned payment attempts in
|
|
135
|
+
the host database.
|
|
136
|
+
test_files: []
|