bsv-wallet-postgres 0.3.1 → 0.4.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69980501d8a03bd6552190800aba40a65f3c34d2b6633a3a59d58030a11d54e3
|
|
4
|
+
data.tar.gz: 7ee7f4c26fc294d9f227cdae83e3c9d9dcc7cce64592dda289db8c223c6b34ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5fa70d4686c6f55d782578f9f7c2b6bb290c113ebdd2a8e05a11d8062e56ba21337a2ce3bf8d96af32a13ac60d05d8328f6608d560f6b2dce3b1514045f6b1ef
|
|
7
|
+
data.tar.gz: 371d0d3ccc53cf8c247aed4fa449a232b0a9f3ce46e8c7ebfbc311f5f23eae7043a186e0678c14fe5f3693ced12e3458965d3a93e471fcdbc2f46a1661961eac
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to the `bsv-wallet-postgres` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.4.0 — 2026-04-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `BSV::Wallet::SolidQueueAdapter` — PostgreSQL-backed async broadcast queue implementing the `BroadcastQueue` interface
|
|
12
|
+
- Migration 006: `wallet_broadcast_jobs` table with `FOR UPDATE SKIP LOCKED` polling support
|
|
13
|
+
- Background worker thread broadcasts transactions and promotes/rolls back wallet state
|
|
14
|
+
- Recovery on restart via stale `locked_at` detection
|
|
15
|
+
- Idempotent enqueue on duplicate txid (crash recovery)
|
|
16
|
+
- `MAX_ATTEMPTS` enforcement (5) prevents infinite retry loops
|
|
17
|
+
- Guard refuses MemoryStore attachment
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Migration timestamps use `timestamptz` (matching migration 004 pattern)
|
|
21
|
+
- `start()` check-and-set is atomic under mutex (prevents TOCTOU double-spawn)
|
|
22
|
+
- Deserialization failures mark job as failed immediately (no tight retry loop)
|
|
23
|
+
|
|
8
24
|
## 0.3.1 — 2026-04-12
|
|
9
25
|
|
|
10
26
|
### Fixed
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Broadcast job queue table for the SolidQueueAdapter.
|
|
4
|
+
#
|
|
5
|
+
# Tracks outbound transactions that need to be broadcast to the network,
|
|
6
|
+
# with state machine, retry counters, and advisory locking support so that
|
|
7
|
+
# multiple worker processes can poll the queue without double-processing.
|
|
8
|
+
#
|
|
9
|
+
# === Columns
|
|
10
|
+
#
|
|
11
|
+
# * +txid+ — TEXT NOT NULL UNIQUE. Prevents duplicate queue entries
|
|
12
|
+
# for the same transaction.
|
|
13
|
+
#
|
|
14
|
+
# * +status+ — TEXT NOT NULL DEFAULT 'unsent'. State machine values:
|
|
15
|
+
# 'unsent', 'sending', 'completed', 'failed'.
|
|
16
|
+
#
|
|
17
|
+
# * +beef_hex+ — TEXT NOT NULL. Serialised BEEF payload to broadcast.
|
|
18
|
+
#
|
|
19
|
+
# * +input_outpoints+ — text[], nullable. Outpoints consumed by the tx, used
|
|
20
|
+
# to release UTXOs on success or rollback on failure.
|
|
21
|
+
#
|
|
22
|
+
# * +change_outpoints+ — text[], nullable. Change outpoints created by the tx,
|
|
23
|
+
# used to mark outputs spendable after confirmation.
|
|
24
|
+
#
|
|
25
|
+
# * +fund_ref+ — TEXT, nullable. Caller-supplied funding reference for
|
|
26
|
+
# observability and reconciliation.
|
|
27
|
+
#
|
|
28
|
+
# * +attempts+ — INTEGER NOT NULL DEFAULT 0. Incremented on each
|
|
29
|
+
# broadcast attempt; gates retry logic.
|
|
30
|
+
#
|
|
31
|
+
# * +last_error+ — TEXT, nullable. Message from the last failed attempt.
|
|
32
|
+
#
|
|
33
|
+
# * +locked_at+ — TIMESTAMPTZ, nullable. Set when a worker claims the
|
|
34
|
+
# job; cleared on completion or stale-lock recovery.
|
|
35
|
+
#
|
|
36
|
+
# * +created_at+ — TIMESTAMPTZ NOT NULL DEFAULT NOW().
|
|
37
|
+
#
|
|
38
|
+
# * +updated_at+ — TIMESTAMPTZ NOT NULL DEFAULT NOW().
|
|
39
|
+
#
|
|
40
|
+
# === Index
|
|
41
|
+
#
|
|
42
|
+
# A composite index on +(status, locked_at)+ (named +broadcast_jobs_poll_idx+)
|
|
43
|
+
# supports the hot-path poll query:
|
|
44
|
+
#
|
|
45
|
+
# WHERE status = 'unsent'
|
|
46
|
+
# OR (status = 'sending' AND locked_at < NOW() - interval '300 seconds'
|
|
47
|
+
# AND attempts < 5)
|
|
48
|
+
# ORDER BY created_at
|
|
49
|
+
# FOR UPDATE SKIP LOCKED
|
|
50
|
+
Sequel.migration do
|
|
51
|
+
change do
|
|
52
|
+
create_table(:wallet_broadcast_jobs) do
|
|
53
|
+
primary_key :id, type: :Bignum
|
|
54
|
+
String :txid, null: false, unique: true
|
|
55
|
+
String :status, null: false, default: 'unsent'
|
|
56
|
+
String :beef_hex, text: true, null: false
|
|
57
|
+
column :input_outpoints, 'text[]'
|
|
58
|
+
column :change_outpoints, 'text[]'
|
|
59
|
+
String :fund_ref
|
|
60
|
+
Integer :attempts, null: false, default: 0
|
|
61
|
+
String :last_error, text: true
|
|
62
|
+
column :locked_at, :timestamptz
|
|
63
|
+
column :created_at, :timestamptz, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
64
|
+
column :updated_at, :timestamptz, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
65
|
+
index %i[status locked_at], name: :broadcast_jobs_poll_idx
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
# PostgreSQL-backed asynchronous broadcast queue adapter.
|
|
8
|
+
#
|
|
9
|
+
# Persists outbound transactions to the +wallet_broadcast_jobs+ table and
|
|
10
|
+
# processes them in a background worker thread. Designed for multi-process
|
|
11
|
+
# deployments where +InlineQueue+'s synchronous broadcast is unacceptable.
|
|
12
|
+
#
|
|
13
|
+
# The worker uses +SELECT ... FOR UPDATE SKIP LOCKED+ so multiple processes
|
|
14
|
+
# can run +SolidQueueAdapter+ against the same database safely — only one
|
|
15
|
+
# worker will claim a given job.
|
|
16
|
+
#
|
|
17
|
+
# === Lifecycle
|
|
18
|
+
#
|
|
19
|
+
# adapter = BSV::Wallet::SolidQueueAdapter.new(
|
|
20
|
+
# db: sequel_db,
|
|
21
|
+
# storage: postgres_store,
|
|
22
|
+
# broadcaster: arc_broadcaster
|
|
23
|
+
# )
|
|
24
|
+
# adapter.start # spawns background worker thread
|
|
25
|
+
# # ... process transactions ...
|
|
26
|
+
# adapter.drain # stop + join (blocks until current poll cycle completes)
|
|
27
|
+
#
|
|
28
|
+
# === Recovery
|
|
29
|
+
#
|
|
30
|
+
# On restart, the worker's first poll naturally finds stale +sending+ jobs
|
|
31
|
+
# (those whose +locked_at+ is older than +stale_threshold+ seconds) and
|
|
32
|
+
# re-broadcasts them. No special recovery code is needed.
|
|
33
|
+
#
|
|
34
|
+
# === Drain warning
|
|
35
|
+
#
|
|
36
|
+
# +drain+ blocks until the current poll cycle completes. If a job is
|
|
37
|
+
# mid-broadcast this may take several seconds. Jobs enqueued after
|
|
38
|
+
# +@running = false+ but before the worker thread exits will remain
|
|
39
|
+
# +unsent+ until the next +start+.
|
|
40
|
+
class SolidQueueAdapter
|
|
41
|
+
include BSV::Wallet::BroadcastQueue
|
|
42
|
+
|
|
43
|
+
# Default number of seconds between poll cycles.
|
|
44
|
+
DEFAULT_POLL_INTERVAL = 8
|
|
45
|
+
|
|
46
|
+
# Default number of seconds before a +sending+ job is considered stale
|
|
47
|
+
# and eligible for retry. Configurable for testability.
|
|
48
|
+
STALE_THRESHOLD = 300
|
|
49
|
+
|
|
50
|
+
# Maximum number of broadcast attempts before a job is abandoned.
|
|
51
|
+
# After this many failures the job remains +failed+ permanently.
|
|
52
|
+
MAX_ATTEMPTS = 5
|
|
53
|
+
|
|
54
|
+
# @param db [Sequel::Database] a Sequel database handle (shared with PostgresStore)
|
|
55
|
+
# @param storage [StorageAdapter] wallet storage adapter (must not be MemoryStore)
|
|
56
|
+
# @param broadcaster [#broadcast] broadcaster object
|
|
57
|
+
# @param poll_interval [Integer] seconds between worker poll cycles
|
|
58
|
+
# @param stale_threshold [Integer] seconds before a +sending+ job is retried
|
|
59
|
+
# @raise [ArgumentError] if +storage+ is a +BSV::Wallet::MemoryStore+
|
|
60
|
+
# @raise [ArgumentError] if +broadcaster+ is +nil+
|
|
61
|
+
def initialize(db:, storage:, broadcaster:, poll_interval: DEFAULT_POLL_INTERVAL, stale_threshold: STALE_THRESHOLD)
|
|
62
|
+
if storage.is_a?(BSV::Wallet::MemoryStore)
|
|
63
|
+
raise ArgumentError, 'SolidQueueAdapter requires a persistent storage adapter — MemoryStore is not supported'
|
|
64
|
+
end
|
|
65
|
+
raise ArgumentError, 'SolidQueueAdapter requires a broadcaster' if broadcaster.nil?
|
|
66
|
+
|
|
67
|
+
@db = db
|
|
68
|
+
@storage = storage
|
|
69
|
+
@broadcaster = broadcaster
|
|
70
|
+
@poll_interval = poll_interval
|
|
71
|
+
@stale_threshold = stale_threshold
|
|
72
|
+
|
|
73
|
+
@mutex = Mutex.new
|
|
74
|
+
@running = false
|
|
75
|
+
@worker_thread = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns +true+ — this adapter executes broadcast asynchronously.
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def async?
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Persists a transaction to the broadcast job queue and returns immediately.
|
|
86
|
+
#
|
|
87
|
+
# Inserts a row into +wallet_broadcast_jobs+ with status +unsent+. If a row
|
|
88
|
+
# already exists for the same +txid+ (e.g. after a crash and restart), the
|
|
89
|
+
# +UniqueConstraintViolation+ is rescued and the existing status is returned
|
|
90
|
+
# instead.
|
|
91
|
+
#
|
|
92
|
+
# @param payload [Hash] broadcast payload (see +BroadcastQueue+ module docs)
|
|
93
|
+
# @return [Hash] +{ txid: String, broadcast_status: 'sending' }+
|
|
94
|
+
def enqueue(payload)
|
|
95
|
+
txid = payload[:txid]
|
|
96
|
+
beef_binary = payload[:beef_binary]
|
|
97
|
+
input_outpoints = payload[:input_outpoints]
|
|
98
|
+
change_outpoints = payload[:change_outpoints]
|
|
99
|
+
fund_ref = payload[:fund_ref]
|
|
100
|
+
|
|
101
|
+
row = {
|
|
102
|
+
txid: txid,
|
|
103
|
+
beef_hex: beef_binary.unpack1('H*'),
|
|
104
|
+
input_outpoints: input_outpoints ? Sequel.pg_array(input_outpoints, :text) : nil,
|
|
105
|
+
change_outpoints: change_outpoints ? Sequel.pg_array(change_outpoints, :text) : nil,
|
|
106
|
+
fund_ref: fund_ref,
|
|
107
|
+
status: 'unsent'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
@db[:wallet_broadcast_jobs].insert(row)
|
|
112
|
+
rescue Sequel::UniqueConstraintViolation
|
|
113
|
+
existing_status = @db[:wallet_broadcast_jobs].where(txid: txid).get(:status)
|
|
114
|
+
return { txid: txid, broadcast_status: existing_status || 'sending' }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
{ txid: txid, broadcast_status: 'sending' }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the broadcast status for a previously enqueued transaction.
|
|
121
|
+
#
|
|
122
|
+
# Reads directly from the jobs table — the authoritative source of truth
|
|
123
|
+
# for async broadcast status.
|
|
124
|
+
#
|
|
125
|
+
# @param txid [String] hex transaction identifier
|
|
126
|
+
# @return [String, nil] status string or +nil+ if no job exists
|
|
127
|
+
def status(txid)
|
|
128
|
+
@db[:wallet_broadcast_jobs].where(txid: txid).get(:status)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Spawns the background worker thread.
|
|
132
|
+
#
|
|
133
|
+
# Safe to call multiple times — returns immediately if already running.
|
|
134
|
+
# The check-and-set is atomic under the mutex to prevent two concurrent
|
|
135
|
+
# +start+ calls from spawning duplicate worker threads.
|
|
136
|
+
#
|
|
137
|
+
# @return [void]
|
|
138
|
+
def start
|
|
139
|
+
@mutex.synchronize do
|
|
140
|
+
return if @running || @worker_thread&.alive?
|
|
141
|
+
|
|
142
|
+
@running = true
|
|
143
|
+
end
|
|
144
|
+
@worker_thread = Thread.new { worker_loop }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Signals the worker to stop after the current poll cycle.
|
|
148
|
+
#
|
|
149
|
+
# Non-blocking — returns immediately without waiting for the thread.
|
|
150
|
+
#
|
|
151
|
+
# @return [void]
|
|
152
|
+
def stop
|
|
153
|
+
@mutex.synchronize { @running = false }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Stops the worker and blocks until the current poll cycle completes.
|
|
157
|
+
#
|
|
158
|
+
# Safe to call when +start+ has not been called (+@worker_thread+ is nil).
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
161
|
+
def drain
|
|
162
|
+
stop
|
|
163
|
+
@worker_thread&.join
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
# Main loop for the background worker thread.
|
|
169
|
+
#
|
|
170
|
+
# Calls +poll_once+ in a loop, sleeping +poll_interval+ seconds between
|
|
171
|
+
# cycles. Unexpected errors from +poll_once+ are logged to stderr and the
|
|
172
|
+
# loop continues — the worker thread must not die silently.
|
|
173
|
+
def worker_loop
|
|
174
|
+
while running?
|
|
175
|
+
begin
|
|
176
|
+
poll_once
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
warn "[bsv-wallet] SolidQueueAdapter worker error: #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
sleep(@poll_interval) if running?
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Thread-safe check of the running flag.
|
|
185
|
+
#
|
|
186
|
+
# @return [Boolean]
|
|
187
|
+
def running?
|
|
188
|
+
@mutex.synchronize { @running }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Claims and processes a single pending broadcast job.
|
|
192
|
+
#
|
|
193
|
+
# Uses +SELECT ... FOR UPDATE SKIP LOCKED+ so concurrent workers do not
|
|
194
|
+
# double-process the same job. Stale +sending+ jobs (those whose +locked_at+
|
|
195
|
+
# is older than +stale_threshold+ seconds) are also eligible for retry,
|
|
196
|
+
# up to +MAX_ATTEMPTS+.
|
|
197
|
+
#
|
|
198
|
+
# NOTE: The entire +process_job+ call (including the network broadcast)
|
|
199
|
+
# runs inside this transaction, holding the row lock and a database
|
|
200
|
+
# connection for the duration. This is acceptable for a single worker
|
|
201
|
+
# thread with an 8-second poll interval but would need restructuring
|
|
202
|
+
# for high-throughput multi-worker deployments.
|
|
203
|
+
#
|
|
204
|
+
# @return [void]
|
|
205
|
+
def poll_once
|
|
206
|
+
@db.transaction do
|
|
207
|
+
job = @db[:wallet_broadcast_jobs]
|
|
208
|
+
.where(Sequel.lit(
|
|
209
|
+
"status = 'unsent' OR " \
|
|
210
|
+
"(status = 'sending' AND locked_at < NOW() - " \
|
|
211
|
+
"#{@stale_threshold.to_i} * interval '1 second' " \
|
|
212
|
+
"AND attempts < #{MAX_ATTEMPTS})"
|
|
213
|
+
))
|
|
214
|
+
.order(:created_at)
|
|
215
|
+
.limit(1)
|
|
216
|
+
.for_update
|
|
217
|
+
.skip_locked
|
|
218
|
+
.first
|
|
219
|
+
return unless job
|
|
220
|
+
|
|
221
|
+
process_job(job)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Marks a job as +sending+, broadcasts the transaction, and promotes or
|
|
226
|
+
# rolls back wallet state based on the outcome.
|
|
227
|
+
#
|
|
228
|
+
# Deserialization failures (corrupt +beef_hex+) are caught and mark the
|
|
229
|
+
# job as +failed+ immediately to prevent infinite retry loops.
|
|
230
|
+
#
|
|
231
|
+
# @param job [Hash] row from +wallet_broadcast_jobs+
|
|
232
|
+
# @return [void]
|
|
233
|
+
def process_job(job)
|
|
234
|
+
@db[:wallet_broadcast_jobs].where(id: job[:id]).update(
|
|
235
|
+
status: 'sending',
|
|
236
|
+
locked_at: Sequel.lit('NOW()'),
|
|
237
|
+
attempts: Sequel.lit('attempts + 1'),
|
|
238
|
+
updated_at: Sequel.lit('NOW()')
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
begin
|
|
242
|
+
tx = BSV::Transaction::Transaction.from_beef_hex(job[:beef_hex])
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
@db[:wallet_broadcast_jobs].where(id: job[:id]).update(
|
|
245
|
+
status: 'failed',
|
|
246
|
+
last_error: "Deserialization failed: #{e.message}",
|
|
247
|
+
updated_at: Sequel.lit('NOW()')
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
input_outpoints = job[:input_outpoints]&.to_a
|
|
253
|
+
change_outpoints = job[:change_outpoints]&.to_a
|
|
254
|
+
txid = job[:txid]
|
|
255
|
+
fund_ref = job[:fund_ref]
|
|
256
|
+
|
|
257
|
+
begin
|
|
258
|
+
@broadcaster.broadcast(tx)
|
|
259
|
+
rescue StandardError => e
|
|
260
|
+
if input_outpoints && !input_outpoints.empty?
|
|
261
|
+
rollback(input_outpoints, change_outpoints, txid, fund_ref)
|
|
262
|
+
elsif txid
|
|
263
|
+
@storage.update_action_status(txid, 'failed')
|
|
264
|
+
end
|
|
265
|
+
@db[:wallet_broadcast_jobs].where(id: job[:id]).update(
|
|
266
|
+
status: 'failed',
|
|
267
|
+
last_error: e.message,
|
|
268
|
+
updated_at: Sequel.lit('NOW()')
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
promote(input_outpoints, change_outpoints, txid)
|
|
274
|
+
@db[:wallet_broadcast_jobs].where(id: job[:id]).update(
|
|
275
|
+
status: 'completed',
|
|
276
|
+
updated_at: Sequel.lit('NOW()')
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Promotes UTXO state after a successful broadcast.
|
|
281
|
+
#
|
|
282
|
+
# Marks inputs as +:spent+, change as +:spendable+, and updates the action
|
|
283
|
+
# status to +completed+. When outpoints are +nil+ (finalize path), UTXO
|
|
284
|
+
# transitions are skipped.
|
|
285
|
+
#
|
|
286
|
+
# @param input_outpoints [Array<String>, nil]
|
|
287
|
+
# @param change_outpoints [Array<String>, nil]
|
|
288
|
+
# @param txid [String, nil]
|
|
289
|
+
def promote(input_outpoints, change_outpoints, txid)
|
|
290
|
+
Array(input_outpoints).each { |op| @storage.update_output_state(op, :spent) }
|
|
291
|
+
Array(change_outpoints).each { |op| @storage.update_output_state(op, :spendable) }
|
|
292
|
+
@storage.update_action_status(txid, 'completed') if txid
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Rolls back wallet state after a failed broadcast.
|
|
296
|
+
#
|
|
297
|
+
# Releases locked inputs (only those matching +fund_ref+), deletes phantom
|
|
298
|
+
# change outputs, and marks the action as +failed+.
|
|
299
|
+
#
|
|
300
|
+
# @param input_outpoints [Array<String>]
|
|
301
|
+
# @param change_outpoints [Array<String>]
|
|
302
|
+
# @param txid [String, nil]
|
|
303
|
+
# @param fund_ref [String] fund reference used when locking inputs
|
|
304
|
+
def rollback(input_outpoints, change_outpoints, txid, fund_ref)
|
|
305
|
+
Array(input_outpoints).each do |op|
|
|
306
|
+
outputs = @storage.find_outputs({ outpoint: op, include_spent: true, limit: 1, offset: 0 })
|
|
307
|
+
next if outputs.empty?
|
|
308
|
+
next unless outputs.first[:pending_reference] == fund_ref
|
|
309
|
+
|
|
310
|
+
@storage.update_output_state(op, :spendable)
|
|
311
|
+
end
|
|
312
|
+
Array(change_outpoints).each { |op| @storage.delete_output(op) }
|
|
313
|
+
@storage.update_action_status(txid, 'failed') if txid
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
data/lib/bsv/wallet_postgres.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-wallet-postgres
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
@@ -72,7 +72,9 @@ files:
|
|
|
72
72
|
- lib/bsv/wallet_postgres/migrations/003_add_wallet_settings.rb
|
|
73
73
|
- lib/bsv/wallet_postgres/migrations/004_add_pending_metadata.rb
|
|
74
74
|
- lib/bsv/wallet_postgres/migrations/005_add_txid_unique_index.rb
|
|
75
|
+
- lib/bsv/wallet_postgres/migrations/006_create_broadcast_jobs.rb
|
|
75
76
|
- lib/bsv/wallet_postgres/postgres_store.rb
|
|
77
|
+
- lib/bsv/wallet_postgres/solid_queue_adapter.rb
|
|
76
78
|
- lib/bsv/wallet_postgres/version.rb
|
|
77
79
|
homepage: https://github.com/sgbett/bsv-ruby-sdk
|
|
78
80
|
licenses:
|