Class BSV::Wallet::PostgresStore ¶
Inherits: Object Includes: BSV::Wallet::Interface::Store
PostgreSQL-backed storage adapter for BSV::Wallet.
Implements the full {Store} interface against a Sequel Database object. Survives process restarts, scales to multiple instances, and is thread-safe via Sequel's connection pool.
Design notes¶
-
JSONB is the source of truth. Every row stores the full record hash in a
datajsonb column; dedicated indexed columns (basket,tags,labels,certifier, ...) exist only to make queries fast. Reads return the jsonb blob so adding fields to bsv-wallet's record hashes does not require a schema change. -
Outputs upsert on
outpoint(unique); certificates upsert on the composite unique +(type, serial_number, certifier)+. Proofs and transactions upsert on theirtxidprimary key. Actions are append-only — the interface has no natural key for actions. -
Pagination is ordered by insertion (+id ASC+) to match MemoryStore.
-
This class is thread-safe because Sequel is — the adapter itself holds no mutable state beyond the injected database handle.
@example Quickstart
require 'bsv-wallet-postgres'
db = Sequel.connect(ENV['DATABASE_URL'])
BSV::Wallet::PostgresStore.migrate!(db)
store = BSV::Wallet::PostgresStore.new(db)
wallet = BSV::Wallet::Client.new(key, storage: store)
@example Bringing your own migration runner
# Copy lib/bsv/wallet_postgres/migrations/001_create_wallet_tables.rb
# into your own db/migrate directory, then run your framework's
# migrator as normal. `migrate!` is a convenience — not a requirement.
Constants¶
MIGRATIONS_DIR ¶
Not documented.
Attributes¶
db [R] ¶
- @return [Sequel::Database] the underlying database handle
Public Class Methods¶
migrate!(db) ¶
Run the shipped wallet schema migrations against db.
Uses Sequel's migrator so every schema change ships as a numbered migration file and the database tracks which ones have been applied. Safe to call repeatedly.
Consumers who prefer their own migration framework can copy the migration file(s) out of lib/bsv/wallet_postgres/migrations/ instead of calling this helper. - @param db [Sequel::Database] - @return [void]
Public Instance Methods¶
count_actions(query) ¶
Not documented.
count_certificates(query) ¶
Not documented.
count_outputs(query) ¶
Not documented.
delete_action(txid) ¶
Not documented.
delete_certificate(type:, serial_number:, certifier:) ¶
Not documented.
delete_output(outpoint) ¶
Not documented.
find_actions(query) ¶
Not documented.
find_certificates(query) ¶
Not documented.
find_outputs(query) ¶
Not documented.
find_proof(txid) ¶
Not documented.
find_setting(key) ¶
Not documented.
find_spendable_outputs(basket: = nil, min_satoshis: = nil, sort_order: = :desc) ¶
Returns outputs whose effective state is :spendable.
Legacy rows with +state = NULL+ are treated as spendable when the spendable boolean is true (or absent), matching MemoryStore's effective_state logic. - @param basket [String, nil] restrict to this basket when provided - @param min_satoshis [Integer, nil] exclude outputs below this value - @param sort_order [Symbol] +:asc+ or +:desc+ (default +:desc+, largest first) - @return [Array
find_transaction(txid) ¶
Not documented.
initialize(db) ¶
- @param
db[Sequel::Database] a Sequel database handle. The caller owns connection lifecycle, pool sizing, and migrations. - @return [PostgresStore] a new instance of PostgresStore
lock_utxos(outpoints, reference:, no_send: = false) ¶
Atomically marks a set of outpoints as :pending.
Uses +UPDATE ... WHERE state = 'spendable' ... RETURNING outpoint+ so that the check-and-set is atomic at the database level. A concurrent caller that wins the race will have already changed the state to 'pending', so the second caller's WHERE clause will not match and will return nothing. No explicit row-level locking is needed — the UPDATE itself takes the lock.
Legacy rows with +state = NULL AND spendable = TRUE+ are also eligible. - @param outpoints [Arrayreference [String] caller-supplied pending reference - @param no_send [Boolean] true if this is a no_send lock - @return [Array
release_stale_pending!(timeout: = 300) ¶
Releases stale pending locks back to :spendable.
Any output in :pending state whose pending_since is older than timeout seconds is reset to spendable and its pending metadata is cleared. Outputs with +no_send = true+ are exempt and remain pending. Outputs with +pending_since = NULL+ are also skipped — they are treated as freshly locked (NULL means "just acquired but no timestamp yet"). - @param timeout [Integer] age in seconds before a lock is considered stale (default 300) - @return [Integer] number of outputs released
store_action(action_data) ¶
--- Actions ---
store_certificate(cert_data) ¶
--- Certificates ---
store_output(output_data) ¶
--- Outputs ---
store_proof(txid, bump_hex) ¶
--- Proofs ---
store_setting(key, value) ¶
--- Settings ---
store_transaction(txid, tx_hex) ¶
--- Transactions ---
update_action_status(txid, new_status) ¶
- @raise [WalletError]
update_output_basket(outpoint, new_basket) ¶
Moves an output from one basket to another (metadata-only).
Updates both the basket column and the JSONB data blob. Does not affect the output's state or pending metadata. - @param outpoint [String] the outpoint identifier - @param new_basket [String] the destination basket name - @raise [BSV::Wallet::WalletError] if the outpoint is not found - @return [Hash] the updated output hash
update_output_state(outpoint, new_state, pending_reference: = nil, no_send: = nil) ¶
Transitions the state of an existing output.
When new_state is :pending, sets pending_since, pending_reference, and no_send, and merges those values into the JSONB data blob.
When transitioning away from :pending, clears the pending metadata columns and removes the corresponding keys from the JSONB blob. - @param outpoint [String] the outpoint identifier - @param new_state [Symbol] +:spendable+, +:pending+, or +:spent+ - @param pending_reference [String, nil] caller-supplied label for a pending lock - @param no_send [Boolean, nil] true if the lock belongs to a no_send transaction - @raise [BSV::Wallet::WalletError] if the outpoint is not found - @return [Hash] the updated output hash