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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -115
  3. data/LICENSE +23 -80
  4. data/db/migrations/001_create_schema.rb +261 -0
  5. data/db/migrations/002_action_id_cascade.rb +66 -0
  6. data/db/migrations/003_schema_constraints.rb +297 -0
  7. data/db/migrations/004_drop_tx_reqs.rb +32 -0
  8. data/lib/bsv/wallet/postgres/action.rb +80 -0
  9. data/lib/bsv/wallet/postgres/action_label.rb +14 -0
  10. data/lib/bsv/wallet/postgres/arc_adapter.rb +32 -0
  11. data/lib/bsv/wallet/postgres/basket.rb +13 -0
  12. data/lib/bsv/wallet/postgres/block.rb +13 -0
  13. data/lib/bsv/wallet/postgres/broadcast.rb +87 -0
  14. data/lib/bsv/wallet/postgres/broadcast_callback.rb +54 -0
  15. data/lib/bsv/wallet/postgres/broadcast_queue.rb +98 -0
  16. data/lib/bsv/wallet/postgres/certificate.rb +13 -0
  17. data/lib/bsv/wallet/postgres/certificate_field.rb +13 -0
  18. data/lib/bsv/wallet/postgres/display_txid.rb +25 -0
  19. data/lib/bsv/wallet/postgres/input.rb +14 -0
  20. data/lib/bsv/wallet/postgres/label.rb +15 -0
  21. data/lib/bsv/wallet/postgres/output.rb +64 -0
  22. data/lib/bsv/wallet/postgres/output_basket.rb +15 -0
  23. data/lib/bsv/wallet/postgres/output_detail.rb +12 -0
  24. data/lib/bsv/wallet/postgres/output_tag.rb +14 -0
  25. data/lib/bsv/wallet/postgres/proof_store.rb +109 -0
  26. data/lib/bsv/wallet/postgres/setting.rb +32 -0
  27. data/lib/bsv/wallet/postgres/spendable.rb +12 -0
  28. data/lib/bsv/wallet/postgres/store.rb +580 -0
  29. data/lib/bsv/wallet/postgres/tag.rb +15 -0
  30. data/lib/bsv/wallet/postgres/tx_proof.rb +16 -0
  31. data/lib/bsv/wallet/postgres/utxo_pool.rb +58 -0
  32. data/lib/bsv/wallet/postgres/version.rb +9 -0
  33. data/lib/bsv/wallet/postgres.rb +77 -0
  34. data/lib/bsv-wallet-postgres.rb +1 -1
  35. metadata +49 -35
  36. data/lib/bsv/wallet_postgres/migrations/001_create_wallet_tables.rb +0 -58
  37. data/lib/bsv/wallet_postgres/migrations/002_add_output_state.rb +0 -33
  38. data/lib/bsv/wallet_postgres/migrations/003_add_wallet_settings.rb +0 -20
  39. data/lib/bsv/wallet_postgres/migrations/004_add_pending_metadata.rb +0 -69
  40. data/lib/bsv/wallet_postgres/migrations/005_add_txid_unique_index.rb +0 -27
  41. data/lib/bsv/wallet_postgres/migrations/006_create_broadcast_jobs.rb +0 -68
  42. data/lib/bsv/wallet_postgres/postgres_store.rb +0 -482
  43. data/lib/bsv/wallet_postgres/solid_queue_adapter.rb +0 -328
  44. data/lib/bsv/wallet_postgres/version.rb +0 -7
  45. 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