Class: BSV::Wallet::Store::Memory
- Inherits:
-
Object
- Object
- BSV::Wallet::Store::Memory
- Includes:
- Interface::Store
- Defined in:
- lib/bsv/wallet/store/memory.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 Store::Memory safe within a single Ruby process.
NOTE: Store::File 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
Store::Memory.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 ⇒ Memory
constructor
A new instance of Memory.
-
#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_basket(outpoint, new_basket) ⇒ Hash
Reassigns the basket of an existing output without altering any other field.
-
#update_output_state(outpoint, new_state, pending_reference: nil, no_send: nil) ⇒ Object
Transitions the state of an existing output.
Constructor Details
#initialize ⇒ Memory
Returns a new instance of Memory.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/bsv/wallet/store/memory.rb', line 41 def initialize @actions = [] @outputs = [] @certificates = [] @proofs = {} @transactions = {} @settings = {} @mutex = Mutex.new return unless self.class.warn_in_production? && production_env? warn '[bsv-wallet] Store::Memory 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
38 39 40 |
# File 'lib/bsv/wallet/store/memory.rb', line 38 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.
33 34 35 |
# File 'lib/bsv/wallet/store/memory.rb', line 33 def self.warn_in_production? @warn_in_production != false end |
Instance Method Details
#count_actions(query) ⇒ Object
82 83 84 |
# File 'lib/bsv/wallet/store/memory.rb', line 82 def count_actions(query) filter_actions(query).length end |
#count_certificates(query) ⇒ Object
255 256 257 |
# File 'lib/bsv/wallet/store/memory.rb', line 255 def count_certificates(query) filter_certificates(query).length end |
#count_outputs(query) ⇒ Object
95 96 97 |
# File 'lib/bsv/wallet/store/memory.rb', line 95 def count_outputs(query) filter_outputs(query).length end |
#delete_action(txid) ⇒ Object
70 71 72 73 74 75 76 |
# File 'lib/bsv/wallet/store/memory.rb', line 70 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
275 276 277 278 279 280 281 282 283 |
# File 'lib/bsv/wallet/store/memory.rb', line 275 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
99 100 101 102 103 104 105 |
# File 'lib/bsv/wallet/store/memory.rb', line 99 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
78 79 80 |
# File 'lib/bsv/wallet/store/memory.rb', line 78 def find_actions(query) apply_pagination(filter_actions(query), query) end |
#find_certificates(query) ⇒ Object
251 252 253 |
# File 'lib/bsv/wallet/store/memory.rb', line 251 def find_certificates(query) apply_pagination(filter_certificates(query), query) end |
#find_outputs(query) ⇒ Object
91 92 93 |
# File 'lib/bsv/wallet/store/memory.rb', line 91 def find_outputs(query) apply_pagination(filter_outputs(query), query) end |
#find_proof(txid) ⇒ Object
263 264 265 |
# File 'lib/bsv/wallet/store/memory.rb', line 263 def find_proof(txid) @proofs[txid] end |
#find_setting(key) ⇒ Object?
Retrieves a named wallet setting.
297 298 299 |
# File 'lib/bsv/wallet/store/memory.rb', line 297 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.
206 207 208 209 210 211 212 213 |
# File 'lib/bsv/wallet/store/memory.rb', line 206 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
271 272 273 |
# File 'lib/bsv/wallet/store/memory.rb', line 271 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.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/bsv/wallet/store/memory.rb', line 173 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.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/bsv/wallet/store/memory.rb', line 223 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
57 58 59 60 |
# File 'lib/bsv/wallet/store/memory.rb', line 57 def store_action(action_data) @actions << action_data action_data end |
#store_certificate(cert_data) ⇒ Object
246 247 248 249 |
# File 'lib/bsv/wallet/store/memory.rb', line 246 def store_certificate(cert_data) @certificates << cert_data cert_data end |
#store_output(output_data) ⇒ Object
86 87 88 89 |
# File 'lib/bsv/wallet/store/memory.rb', line 86 def store_output(output_data) @outputs << output_data output_data end |
#store_proof(txid, bump_hex) ⇒ Object
259 260 261 |
# File 'lib/bsv/wallet/store/memory.rb', line 259 def store_proof(txid, bump_hex) @proofs[txid] = bump_hex end |
#store_setting(key, value) ⇒ Object
Persists a named wallet setting.
289 290 291 |
# File 'lib/bsv/wallet/store/memory.rb', line 289 def store_setting(key, value) @settings[key] = value end |
#store_transaction(txid, tx_hex) ⇒ Object
267 268 269 |
# File 'lib/bsv/wallet/store/memory.rb', line 267 def store_transaction(txid, tx_hex) @transactions[txid] = tx_hex end |
#update_action_status(txid, new_status) ⇒ Object
62 63 64 65 66 67 68 |
# File 'lib/bsv/wallet/store/memory.rb', line 62 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_basket(outpoint, new_basket) ⇒ Hash
Reassigns the basket of an existing output without altering any other field.
154 155 156 157 158 159 160 161 162 |
# File 'lib/bsv/wallet/store/memory.rb', line 154 def update_output_basket(outpoint, new_basket) @mutex.synchronize do output = @outputs.find { |o| o[:outpoint] == outpoint } raise WalletError, "Output not found: #{outpoint}" unless output output[:basket] = new_basket output end 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.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/bsv/wallet/store/memory.rb', line 127 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 |