bsv-wallet-postgres 0.5.0 → 0.100.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 +4 -4
- data/CHANGELOG.md +17 -115
- data/LICENSE +23 -80
- data/db/migrations/001_create_schema.rb +261 -0
- data/db/migrations/002_action_id_cascade.rb +66 -0
- data/db/migrations/003_schema_constraints.rb +297 -0
- data/db/migrations/004_drop_tx_reqs.rb +32 -0
- data/lib/bsv/wallet/postgres/action.rb +80 -0
- data/lib/bsv/wallet/postgres/action_label.rb +14 -0
- data/lib/bsv/wallet/postgres/arc_adapter.rb +32 -0
- data/lib/bsv/wallet/postgres/basket.rb +13 -0
- data/lib/bsv/wallet/postgres/block.rb +13 -0
- data/lib/bsv/wallet/postgres/broadcast.rb +87 -0
- data/lib/bsv/wallet/postgres/broadcast_callback.rb +54 -0
- data/lib/bsv/wallet/postgres/broadcast_queue.rb +98 -0
- data/lib/bsv/wallet/postgres/certificate.rb +13 -0
- data/lib/bsv/wallet/postgres/certificate_field.rb +13 -0
- data/lib/bsv/wallet/postgres/display_txid.rb +25 -0
- data/lib/bsv/wallet/postgres/input.rb +14 -0
- data/lib/bsv/wallet/postgres/label.rb +15 -0
- data/lib/bsv/wallet/postgres/output.rb +64 -0
- data/lib/bsv/wallet/postgres/output_basket.rb +15 -0
- data/lib/bsv/wallet/postgres/output_detail.rb +12 -0
- data/lib/bsv/wallet/postgres/output_tag.rb +14 -0
- data/lib/bsv/wallet/postgres/proof_store.rb +109 -0
- data/lib/bsv/wallet/postgres/setting.rb +32 -0
- data/lib/bsv/wallet/postgres/spendable.rb +12 -0
- data/lib/bsv/wallet/postgres/store.rb +580 -0
- data/lib/bsv/wallet/postgres/tag.rb +15 -0
- data/lib/bsv/wallet/postgres/tx_proof.rb +16 -0
- data/lib/bsv/wallet/postgres/utxo_pool.rb +58 -0
- data/lib/bsv/wallet/postgres/version.rb +9 -0
- data/lib/bsv/wallet/postgres.rb +77 -0
- data/lib/bsv-wallet-postgres.rb +1 -1
- metadata +49 -35
- data/lib/bsv/wallet_postgres/migrations/001_create_wallet_tables.rb +0 -58
- data/lib/bsv/wallet_postgres/migrations/002_add_output_state.rb +0 -33
- data/lib/bsv/wallet_postgres/migrations/003_add_wallet_settings.rb +0 -20
- data/lib/bsv/wallet_postgres/migrations/004_add_pending_metadata.rb +0 -69
- data/lib/bsv/wallet_postgres/migrations/005_add_txid_unique_index.rb +0 -27
- data/lib/bsv/wallet_postgres/migrations/006_create_broadcast_jobs.rb +0 -68
- data/lib/bsv/wallet_postgres/postgres_store.rb +0 -482
- data/lib/bsv/wallet_postgres/solid_queue_adapter.rb +0 -328
- data/lib/bsv/wallet_postgres/version.rb +0 -7
- data/lib/bsv/wallet_postgres.rb +0 -13
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
# Broadcast lifecycle manager backed by the broadcasts table.
|
|
7
|
+
#
|
|
8
|
+
# Owns network communication for transaction broadcasts. Uses
|
|
9
|
+
# Services for push/fetch operations and delegates column updates
|
|
10
|
+
# to Broadcast#write!.
|
|
11
|
+
class BroadcastQueue
|
|
12
|
+
include BSV::Wallet::Interface::BroadcastQueue
|
|
13
|
+
|
|
14
|
+
def initialize(db: nil, services: nil)
|
|
15
|
+
@db = db || BSV::Wallet::Postgres.db
|
|
16
|
+
@services = services
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def submit(action_id:, raw_tx:, immediate: false)
|
|
20
|
+
BSV.logger&.debug { "[BroadcastQueue] submit: action_id=#{action_id} immediate=#{immediate}" }
|
|
21
|
+
broadcast = Broadcast.create(action_id: action_id)
|
|
22
|
+
|
|
23
|
+
if immediate && @services
|
|
24
|
+
@services.push!(broadcast)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
broadcast_to_hash(broadcast.reload)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process_pending(limit: 100)
|
|
31
|
+
stale = Broadcast
|
|
32
|
+
.where { broadcast_at < Time.now - Broadcast::FETCH_STALENESS }
|
|
33
|
+
.where(Sequel.|({ tx_status: nil }, Sequel.~(tx_status: Broadcast::TERMINAL_STATUSES)))
|
|
34
|
+
.limit(limit)
|
|
35
|
+
.all
|
|
36
|
+
|
|
37
|
+
stale.filter_map do |broadcast|
|
|
38
|
+
next unless broadcast.action&.wtxid && @services
|
|
39
|
+
|
|
40
|
+
result = @services.fetch!(broadcast)
|
|
41
|
+
next unless result.http_success?
|
|
42
|
+
|
|
43
|
+
broadcast_to_hash(broadcast.reload)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_event(event)
|
|
48
|
+
BSV::Primitives::Hex.validate_wtxid!(event[:wtxid], name: 'handle_event wtxid')
|
|
49
|
+
BSV.logger&.debug { "[BroadcastQueue] handle_event: dtxid=#{event[:wtxid].reverse.unpack1('H*')} status=#{event[:tx_status]}" }
|
|
50
|
+
action = Action.first(wtxid: Sequel.blob(event[:wtxid]))
|
|
51
|
+
return unless action
|
|
52
|
+
|
|
53
|
+
broadcast = Broadcast.first(action_id: action.id)
|
|
54
|
+
broadcast ||= Broadcast.create(action_id: action.id)
|
|
55
|
+
|
|
56
|
+
broadcast.update(
|
|
57
|
+
tx_status: event[:tx_status],
|
|
58
|
+
arc_status: event[:status],
|
|
59
|
+
block_hash: event[:block_hash] ? Sequel.blob(event[:block_hash]) : nil,
|
|
60
|
+
block_height: event[:block_height],
|
|
61
|
+
merkle_path: event[:merkle_path] ? Sequel.blob(event[:merkle_path]) : nil,
|
|
62
|
+
extra_info: event[:extra_info],
|
|
63
|
+
competing_txs: event[:competing_txs] ? Sequel.pg_array(event[:competing_txs]) : nil
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
action_id: action.id,
|
|
68
|
+
tx_status: broadcast.tx_status,
|
|
69
|
+
block_hash: broadcast.block_hash,
|
|
70
|
+
block_height: broadcast.block_height,
|
|
71
|
+
merkle_path: broadcast.merkle_path
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def status(action_id:)
|
|
76
|
+
broadcast = Broadcast.first(action_id: action_id)
|
|
77
|
+
return unless broadcast
|
|
78
|
+
|
|
79
|
+
broadcast_to_hash(broadcast)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def broadcast_to_hash(record)
|
|
85
|
+
{
|
|
86
|
+
action_id: record.action_id,
|
|
87
|
+
tx_status: record.tx_status,
|
|
88
|
+
arc_status: record.arc_status,
|
|
89
|
+
broadcast_at: record.broadcast_at,
|
|
90
|
+
block_hash: record.block_hash,
|
|
91
|
+
block_height: record.block_height,
|
|
92
|
+
merkle_path: record.merkle_path
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Certificate < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
one_to_many :certificate_fields, class: 'BSV::Wallet::Postgres::CertificateField'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class CertificateField < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
many_to_one :certificate, class: 'BSV::Wallet::Postgres::Certificate'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
# Convenience method for models with a wtxid column.
|
|
7
|
+
# Converts wire-order binary wtxid to display-order hex — the
|
|
8
|
+
# human-readable format used by block explorers and external APIs.
|
|
9
|
+
#
|
|
10
|
+
# Include on any Sequel::Model whose table has a wtxid bytea column.
|
|
11
|
+
module DisplayTxid
|
|
12
|
+
# Display-order hex transaction ID (reversed from wire-order wtxid).
|
|
13
|
+
#
|
|
14
|
+
# @return [String, nil] 64-character hex string, or nil if wtxid is nil
|
|
15
|
+
def dtxid
|
|
16
|
+
return unless wtxid
|
|
17
|
+
|
|
18
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: "#{self.class.name}#dtxid")
|
|
19
|
+
wtxid.reverse.unpack1('H*')
|
|
20
|
+
end
|
|
21
|
+
alias dtxid_hex dtxid
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Input < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
many_to_one :action, class: 'BSV::Wallet::Postgres::Action'
|
|
10
|
+
many_to_one :output, class: 'BSV::Wallet::Postgres::Output'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Label < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
many_to_many :actions, class: 'BSV::Wallet::Postgres::Action',
|
|
10
|
+
join_table: :action_labels,
|
|
11
|
+
left_key: :label_id, right_key: :action_id
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Output < Sequel::Model
|
|
7
|
+
# No timestamps plugin — outputs are immutable.
|
|
8
|
+
# created_at is set by the database default.
|
|
9
|
+
|
|
10
|
+
many_to_one :action, class: 'BSV::Wallet::Postgres::Action'
|
|
11
|
+
one_to_one :spendable_entry, class: 'BSV::Wallet::Postgres::Spendable', key: :output_id
|
|
12
|
+
one_to_one :detail, class: 'BSV::Wallet::Postgres::OutputDetail', key: :output_id
|
|
13
|
+
one_to_one :input, class: 'BSV::Wallet::Postgres::Input', key: :output_id
|
|
14
|
+
one_to_one :output_basket, class: 'BSV::Wallet::Postgres::OutputBasket', key: :output_id
|
|
15
|
+
many_to_many :tags, class: 'BSV::Wallet::Postgres::Tag',
|
|
16
|
+
join_table: :output_tags,
|
|
17
|
+
left_key: :output_id, right_key: :tag_id
|
|
18
|
+
|
|
19
|
+
dataset_module do
|
|
20
|
+
# The UTXO set: outputs in the spendable table and not claimed by any input.
|
|
21
|
+
def spendable
|
|
22
|
+
spendable_ds = BSV::Wallet::Postgres::Spendable.dataset
|
|
23
|
+
.where(output_id: Sequel[:outputs][:id]).select(1)
|
|
24
|
+
input_ds = BSV::Wallet::Postgres::Input.dataset
|
|
25
|
+
.where(output_id: Sequel[:outputs][:id]).select(1)
|
|
26
|
+
|
|
27
|
+
where(spendable_ds.exists).exclude(input_ds.exists)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Filter outputs belonging to a named basket.
|
|
31
|
+
# 'default' is implicit — outputs with no output_baskets row.
|
|
32
|
+
def in_basket(name)
|
|
33
|
+
if name == 'default'
|
|
34
|
+
basket_ds = BSV::Wallet::Postgres::OutputBasket.dataset
|
|
35
|
+
.where(Sequel[:output_baskets][:output_id] => Sequel[:outputs][:id])
|
|
36
|
+
.select(1)
|
|
37
|
+
exclude(basket_ds.exists)
|
|
38
|
+
else
|
|
39
|
+
basket_ds = BSV::Wallet::Postgres::OutputBasket.dataset
|
|
40
|
+
.join(:baskets, id: :basket_id)
|
|
41
|
+
.where(Sequel[:output_baskets][:output_id] => Sequel[:outputs][:id])
|
|
42
|
+
.where(Sequel[:baskets][:name] => name)
|
|
43
|
+
.select(1)
|
|
44
|
+
where(basket_ds.exists)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Filter outputs with at least the given satoshi value.
|
|
49
|
+
def min_satoshis(value)
|
|
50
|
+
where { satoshis >= value }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def spendable?
|
|
55
|
+
!spendable_entry.nil? && input.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def basket
|
|
59
|
+
output_basket&.basket
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class OutputBasket < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
many_to_one :output, class: 'BSV::Wallet::Postgres::Output'
|
|
10
|
+
many_to_one :basket, class: 'BSV::Wallet::Postgres::Basket'
|
|
11
|
+
many_to_one :action, class: 'BSV::Wallet::Postgres::Action'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class OutputDetail < Sequel::Model
|
|
7
|
+
many_to_one :output, class: 'BSV::Wallet::Postgres::Output'
|
|
8
|
+
many_to_one :action, class: 'BSV::Wallet::Postgres::Action'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class OutputTag < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
many_to_one :output, class: 'BSV::Wallet::Postgres::Output'
|
|
10
|
+
many_to_one :tag, class: 'BSV::Wallet::Postgres::Tag'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
# Merkle proof manager backed by tx_proofs table.
|
|
7
|
+
class ProofStore
|
|
8
|
+
include BSV::Wallet::Interface::ProofStore
|
|
9
|
+
|
|
10
|
+
def initialize(db: nil)
|
|
11
|
+
@db = db || BSV::Wallet::Postgres.db
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save_proof(wtxid:, proof:)
|
|
15
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'save_proof wtxid')
|
|
16
|
+
BSV.logger&.debug { "[ProofStore] save_proof: dtxid=#{wtxid.reverse.unpack1('H*')} height=#{proof[:height]}" }
|
|
17
|
+
|
|
18
|
+
block_id = find_or_create_block(proof) if proof[:height]
|
|
19
|
+
|
|
20
|
+
existing = TxProof.first(wtxid: Sequel.blob(wtxid))
|
|
21
|
+
cols = proof_columns(proof).merge(block_id ? { block_id: block_id } : {})
|
|
22
|
+
if existing
|
|
23
|
+
existing.update(cols)
|
|
24
|
+
existing.id
|
|
25
|
+
else
|
|
26
|
+
TxProof.create({ wtxid: wtxid }.merge(cols)).id
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def find_proof(wtxid:)
|
|
31
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'find_proof wtxid')
|
|
32
|
+
record = TxProof.first(wtxid: Sequel.blob(wtxid))
|
|
33
|
+
return unless record
|
|
34
|
+
|
|
35
|
+
proof_to_hash(record)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def proof_exists?(wtxid:)
|
|
39
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'proof_exists? wtxid')
|
|
40
|
+
TxProof.where(wtxid: Sequel.blob(wtxid)).any?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def proof_columns(proof)
|
|
46
|
+
cols = {}
|
|
47
|
+
cols[:block_index] = proof[:block_index] if proof.key?(:block_index)
|
|
48
|
+
cols[:merkle_path] = proof[:merkle_path] ? Sequel.blob(proof[:merkle_path]) : nil if proof.key?(:merkle_path)
|
|
49
|
+
cols[:raw_tx] = proof[:raw_tx] ? Sequel.blob(proof[:raw_tx]) : nil if proof.key?(:raw_tx)
|
|
50
|
+
cols
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def proof_to_hash(record)
|
|
54
|
+
block = record.block
|
|
55
|
+
{
|
|
56
|
+
id: record.id,
|
|
57
|
+
wtxid: record.wtxid,
|
|
58
|
+
block_id: record.block_id,
|
|
59
|
+
height: block&.height,
|
|
60
|
+
block_index: record.block_index,
|
|
61
|
+
merkle_path: record.merkle_path,
|
|
62
|
+
raw_tx: record.raw_tx,
|
|
63
|
+
block_hash: block&.block_hash,
|
|
64
|
+
merkle_root: block&.merkle_root
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns block ID if a block can be found or created, nil otherwise.
|
|
69
|
+
# Derives merkle_root from merkle_path when not provided explicitly.
|
|
70
|
+
# Returns nil only when neither merkle_root nor merkle_path is available.
|
|
71
|
+
def find_or_create_block(proof)
|
|
72
|
+
height = proof[:height]
|
|
73
|
+
return unless height
|
|
74
|
+
|
|
75
|
+
existing = Block.first(height: height)
|
|
76
|
+
return existing.id if existing
|
|
77
|
+
|
|
78
|
+
merkle_root = proof[:merkle_root] || derive_merkle_root(proof[:merkle_path])
|
|
79
|
+
return unless merkle_root
|
|
80
|
+
|
|
81
|
+
Block.create(
|
|
82
|
+
height: height,
|
|
83
|
+
merkle_root: merkle_root,
|
|
84
|
+
block_hash: proof[:block_hash]
|
|
85
|
+
).id
|
|
86
|
+
rescue Sequel::UniqueConstraintViolation
|
|
87
|
+
Block.first!(height: height).id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def derive_merkle_root(merkle_path_binary)
|
|
91
|
+
return unless merkle_path_binary
|
|
92
|
+
|
|
93
|
+
paths = BSV::Transaction::MerklePath.from_binary(merkle_path_binary)
|
|
94
|
+
mp = paths.is_a?(Array) ? paths.first : paths
|
|
95
|
+
mp&.compute_root
|
|
96
|
+
rescue StandardError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def decode_hex(hex)
|
|
101
|
+
return unless hex
|
|
102
|
+
return hex if hex.encoding == Encoding::BINARY
|
|
103
|
+
|
|
104
|
+
[hex].pack('H*')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Setting < Sequel::Model
|
|
7
|
+
plugin :timestamps, update_on_create: true
|
|
8
|
+
|
|
9
|
+
# Retrieve a setting value by key.
|
|
10
|
+
#
|
|
11
|
+
# @param key [String]
|
|
12
|
+
# @return [String, nil]
|
|
13
|
+
def self.get(key)
|
|
14
|
+
first(key: key)&.value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set a setting value (upsert).
|
|
18
|
+
#
|
|
19
|
+
# @param key [String]
|
|
20
|
+
# @param value [String]
|
|
21
|
+
def self.set(key, value)
|
|
22
|
+
record = first(key: key)
|
|
23
|
+
if record
|
|
24
|
+
record.update(value: value)
|
|
25
|
+
else
|
|
26
|
+
create(key: key, value: value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Postgres
|
|
6
|
+
class Spendable < Sequel::Model(:spendable)
|
|
7
|
+
many_to_one :output, class: 'BSV::Wallet::Postgres::Output'
|
|
8
|
+
many_to_one :action, class: 'BSV::Wallet::Postgres::Action'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|