Class: BSV::Wallet::MemoryStore

Inherits:
Object
  • Object
show all
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

FileStore

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeMemoryStore

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

Parameters:

  • value

    the value to set the attribute warn_in_production to.



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.

Returns:

  • (Boolean)


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.

Parameters:

  • key (String)

    the setting name

Returns:

  • (Object, nil)

    the stored value, or nil if not found



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.

Parameters:

  • basket (String, nil) (defaults to: nil)

    restrict to this basket when provided

  • min_satoshis (Integer, nil) (defaults to: nil)

    exclude outputs below this value

  • sort_order (Symbol) (defaults to: :desc)

    :asc or :desc (default :desc, largest first)

Returns:

  • (Array<Hash>)


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.

Parameters:

  • outpoints (Array<String>)

    outpoint identifiers to lock

  • reference (String)

    caller-supplied pending reference

  • no_send (Boolean) (defaults to: false)

    true if this is a no_send lock

Returns:

  • (Array<String>)

    outpoints successfully locked



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.

Parameters:

  • timeout (Integer) (defaults to: 300)

    lock age in seconds before it is considered stale (default 300)

Returns:

  • (Integer)

    number of outputs released



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.

Parameters:

  • key (String)

    the setting name

  • value (Object)

    the setting value (must be JSON-serialisable)



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

Raises:



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.

Parameters:

  • outpoint (String)

    the outpoint identifier

  • new_state (Symbol)

    :spendable, :pending, or :spent

  • pending_reference (String, nil) (defaults to: nil)

    caller-supplied label for the lock

  • no_send (Boolean, nil) (defaults to: nil)

    true if the lock is for a no_send transaction

Raises:



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