Class: BSV::Wallet::MemoryStore
- Inherits:
-
Object
- Object
- BSV::Wallet::MemoryStore
- Includes:
- StorageAdapter
- Defined in:
- lib/bsv/wallet_interface/memory_store.rb
Overview
In-memory storage adapter intended for testing and development only.
Stores actions, outputs, and certificates in plain Ruby arrays. All data is lost when the process exits — do not use in production. Use PostgresStore (or another persistent adapter) for production wallets.
Thread safety: a Mutex serialises all state-mutating operations so that concurrent threads cannot select and mark the same UTXO as pending. This makes MemoryStore safe within a single Ruby process.
NOTE: FileStore is NOT process-safe — concurrent processes share no in-memory lock and may read stale state from disk.
Production Warning
When RACK_ENV, RAILS_ENV, or APP_ENV is set to production or staging, a warning is emitted to stderr. Suppress it with either:
BSV_MEMORY_STORE_OK=1 # environment variable
MemoryStore.warn_in_production = false # Ruby flag (e.g. in test setup)
Direct Known Subclasses
Class Attribute Summary collapse
-
.warn_in_production ⇒ Object
writeonly
Sets the attribute warn_in_production.
Class Method Summary collapse
-
.warn_in_production? ⇒ Boolean
Controls whether the production-environment warning is emitted.
Instance Method Summary collapse
- #count_actions(query) ⇒ Object
- #count_certificates(query) ⇒ Object
- #count_outputs(query) ⇒ Object
- #delete_action(txid) ⇒ Object
- #delete_certificate(type:, serial_number:, certifier:) ⇒ Object
- #delete_output(outpoint) ⇒ Object
- #find_actions(query) ⇒ Object
- #find_certificates(query) ⇒ Object
- #find_outputs(query) ⇒ Object
- #find_proof(txid) ⇒ Object
-
#find_setting(key) ⇒ Object?
Retrieves a named wallet setting.
-
#find_spendable_outputs(basket: nil, min_satoshis: nil, sort_order: :desc) ⇒ Array<Hash>
Returns only outputs whose effective state is
:spendable. - #find_transaction(txid) ⇒ Object
-
#initialize ⇒ MemoryStore
constructor
A new instance of MemoryStore.
-
#lock_utxos(outpoints, reference:, no_send: false) ⇒ Array<String>
Atomically locks the specified outpoints as
:pending. -
#release_stale_pending!(timeout: 300) ⇒ Integer
Releases pending locks that have been held longer than
timeoutseconds. - #store_action(action_data) ⇒ Object
- #store_certificate(cert_data) ⇒ Object
- #store_output(output_data) ⇒ Object
- #store_proof(txid, bump_hex) ⇒ Object
-
#store_setting(key, value) ⇒ Object
Persists a named wallet setting.
- #store_transaction(txid, tx_hex) ⇒ Object
- #update_action_status(txid, new_status) ⇒ Object
-
#update_output_state(outpoint, new_state, pending_reference: nil, no_send: nil) ⇒ Object
Transitions the state of an existing output.
Constructor Details
#initialize ⇒ MemoryStore
Returns a new instance of MemoryStore.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 40 def initialize @actions = [] @outputs = [] @certificates = [] @proofs = {} @transactions = {} @settings = {} @mutex = Mutex.new return unless self.class.warn_in_production? && production_env? warn '[bsv-wallet] MemoryStore is intended for testing only. ' \ 'Use PostgresStore for production wallets. ' \ 'Set BSV_MEMORY_STORE_OK=1 to silence this warning.' end |
Class Attribute Details
.warn_in_production=(value) ⇒ Object (writeonly)
Sets the attribute warn_in_production
37 38 39 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 37 def warn_in_production=(value) @warn_in_production = value end |
Class Method Details
.warn_in_production? ⇒ Boolean
Controls whether the production-environment warning is emitted. Set to false in test suites to silence the warning globally.
32 33 34 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 32 def self.warn_in_production? @warn_in_production != false end |
Instance Method Details
#count_actions(query) ⇒ Object
81 82 83 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 81 def count_actions(query) filter_actions(query).length end |
#count_certificates(query) ⇒ Object
238 239 240 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 238 def count_certificates(query) filter_certificates(query).length end |
#count_outputs(query) ⇒ Object
94 95 96 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 94 def count_outputs(query) filter_outputs(query).length end |
#delete_action(txid) ⇒ Object
69 70 71 72 73 74 75 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 69 def delete_action(txid) idx = @actions.index { |a| a[:txid] == txid } return false unless idx @actions.delete_at(idx) true end |
#delete_certificate(type:, serial_number:, certifier:) ⇒ Object
258 259 260 261 262 263 264 265 266 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 258 def delete_certificate(type:, serial_number:, certifier:) idx = @certificates.index do |c| c[:type] == type && c[:serial_number] == serial_number && c[:certifier] == certifier end return false unless idx @certificates.delete_at(idx) true end |
#delete_output(outpoint) ⇒ Object
98 99 100 101 102 103 104 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 98 def delete_output(outpoint) idx = @outputs.index { |o| o[:outpoint] == outpoint } return false unless idx @outputs.delete_at(idx) true end |
#find_actions(query) ⇒ Object
77 78 79 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 77 def find_actions(query) apply_pagination(filter_actions(query), query) end |
#find_certificates(query) ⇒ Object
234 235 236 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 234 def find_certificates(query) apply_pagination(filter_certificates(query), query) end |
#find_outputs(query) ⇒ Object
90 91 92 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 90 def find_outputs(query) apply_pagination(filter_outputs(query), query) end |
#find_proof(txid) ⇒ Object
246 247 248 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 246 def find_proof(txid) @proofs[txid] end |
#find_setting(key) ⇒ Object?
Retrieves a named wallet setting.
280 281 282 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 280 def find_setting(key) @settings[key] end |
#find_spendable_outputs(basket: nil, min_satoshis: nil, sort_order: :desc) ⇒ Array<Hash>
Returns only outputs whose effective state is :spendable.
Legacy outputs that carry no :state key are treated as spendable when spendable: is not explicitly false.
This method is wrapped in the same mutex as #update_output_state so that a thread cannot select a UTXO that another thread is simultaneously marking as pending.
189 190 191 192 193 194 195 196 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 189 def find_spendable_outputs(basket: nil, min_satoshis: nil, sort_order: :desc) @mutex.synchronize do results = @outputs.select { |o| effective_state(o) == :spendable } results = results.select { |o| o[:basket] == basket } if basket results = results.select { |o| (o[:satoshis] || 0) >= min_satoshis } if min_satoshis results.sort_by { |o| sort_order == :asc ? (o[:satoshis] || 0) : -(o[:satoshis] || 0) } end end |
#find_transaction(txid) ⇒ Object
254 255 256 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 254 def find_transaction(txid) @transactions[txid] end |
#lock_utxos(outpoints, reference:, no_send: false) ⇒ Array<String>
Atomically locks the specified outpoints as :pending.
Holds the mutex for the entire operation so no other thread can read or transition these outputs between the check and the lock.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 156 def lock_utxos(outpoints, reference:, no_send: false) now = Time.now.utc.iso8601 locked = [] @mutex.synchronize do outpoints.each do |op| output = @outputs.find { |o| o[:outpoint] == op } next unless output && effective_state(output) == :spendable output[:state] = :pending output[:pending_since] = now output[:pending_reference] = reference no_send ? output[:no_send] = true : output.delete(:no_send) locked << op end end locked end |
#release_stale_pending!(timeout: 300) ⇒ Integer
Releases pending locks that have been held longer than timeout seconds.
Each output in :pending state whose :pending_since timestamp is older than timeout seconds is reverted to :spendable and its pending metadata is cleared.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 206 def release_stale_pending!(timeout: 300) cutoff = Time.now.utc - timeout released = 0 @mutex.synchronize do @outputs.each do |output| next unless effective_state(output) == :pending next if output[:no_send] next unless output[:pending_since] locked_at = Time.parse(output[:pending_since]) next unless locked_at < cutoff output[:state] = :spendable output.delete(:pending_since) output.delete(:pending_reference) released += 1 end end released end |
#store_action(action_data) ⇒ Object
56 57 58 59 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 56 def store_action(action_data) @actions << action_data action_data end |
#store_certificate(cert_data) ⇒ Object
229 230 231 232 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 229 def store_certificate(cert_data) @certificates << cert_data cert_data end |
#store_output(output_data) ⇒ Object
85 86 87 88 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 85 def store_output(output_data) @outputs << output_data output_data end |
#store_proof(txid, bump_hex) ⇒ Object
242 243 244 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 242 def store_proof(txid, bump_hex) @proofs[txid] = bump_hex end |
#store_setting(key, value) ⇒ Object
Persists a named wallet setting.
272 273 274 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 272 def store_setting(key, value) @settings[key] = value end |
#store_transaction(txid, tx_hex) ⇒ Object
250 251 252 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 250 def store_transaction(txid, tx_hex) @transactions[txid] = tx_hex end |
#update_action_status(txid, new_status) ⇒ Object
61 62 63 64 65 66 67 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 61 def update_action_status(txid, new_status) action = @actions.find { |a| a[:txid] == txid } raise WalletError, "Action not found: #{txid}" unless action action[:status] = new_status action end |
#update_output_state(outpoint, new_state, pending_reference: nil, no_send: nil) ⇒ Object
Transitions the state of an existing output.
When new_state is :pending, a :pending_since (ISO 8601 UTC) and :pending_reference are attached to the output so stale locks can be detected via #release_stale_pending!.
Pass no_send: true to mark the lock as belonging to a no_send transaction; these locks are exempt from automatic stale recovery and must be released explicitly via abort_action.
When transitioning away from :pending, all pending metadata is cleared.
This method is wrapped in a mutex to prevent concurrent transitions on the same output from two threads.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/bsv/wallet_interface/memory_store.rb', line 126 def update_output_state(outpoint, new_state, pending_reference: nil, no_send: nil) @mutex.synchronize do output = @outputs.find { |o| o[:outpoint] == outpoint } raise WalletError, "Output not found: #{outpoint}" unless output output[:state] = new_state if new_state == :pending output[:pending_since] = Time.now.utc.iso8601 output[:pending_reference] = pending_reference no_send ? output[:no_send] = true : output.delete(:no_send) else output.delete(:pending_since) output.delete(:pending_reference) output.delete(:no_send) end output end end |