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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37e390c1a01dfcca455d47cd9f7f9f50d7ccaa7dc8ebed8b6cde03c7037efaac
4
- data.tar.gz: cbb545f0833e6f43dde28ed1fb8677de53874f06fea8798052da8cdfb4172868
3
+ metadata.gz: 828e610f2e6640f09db4de4dddad8e86fa25b6416610b57c026852c73abdb784
4
+ data.tar.gz: 3d89fc8d066a3c4363a295942e1644efaa1e8535971ab6a4cbe81f5a01a282ae
5
5
  SHA512:
6
- metadata.gz: 9861cc380e77b9d726ab69ce61d9c2401962540ed1617512d615197e123bf1c451b30d4028a9c781abe27c89eee41243b72473410ac613d3dfbac6f129255a5f
7
- data.tar.gz: 9d05a5dac568c660f86963e90d7b2deb0852126b63678395574154f819241e791413ccba139eaaea67ee8009175ba71095d1a58b64dd82332ddfa6b6b0699c9e
6
+ metadata.gz: 98e82a0962d6005cb7c76eee3bee04a0a8a52e834462056a34ce417edb5b1c5bc46eef792c8ab32e33e50e02d454b56e5dfd72017bd42a86aa248146cf767732
7
+ data.tar.gz: 6598af72c9d416a533e4939a2a1746034d006b77f86395bdebd4cd4863bd34ae68e2548c88ace670adecddce2fd5f6ad7fef4065b14c1512a24d29b95ae9d2c7
data/CHANGELOG.md CHANGED
@@ -1,125 +1,27 @@
1
- # Changelog — bsv-wallet-postgres
1
+ # Changelog
2
2
 
3
- All notable changes to the `bsv-wallet-postgres` gem are documented here.
3
+ ## [0.100.0] - 2026-05-13
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
- and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## 0.5.0 — 2026-04-16
9
-
10
- ### Changed — **Breaking**
11
-
12
- - `SolidQueueAdapter` now sets the wallet action status to `'unproven'` instead of `'completed'` on successful broadcast. Aligns with the status taxonomy change in bsv-wallet 0.9.0 (HLR #455). The `wallet_broadcast_jobs` row status is unchanged (job queue lifecycle is a separate domain).
5
+ First release of the PostgreSQL adapter for the Ruby BRC-100 wallet.
13
6
 
14
7
  ### Added
15
-
16
- - `BroadcastQueue#broadcast_enabled?` interface method; `SolidQueueAdapter` returns `true` (broadcaster required at construction time). Enables `WalletClient#broadcast_enabled?` to detect a queue-embedded broadcaster.
8
+ - **Schema** — actions, outputs, inputs, spendable, baskets, labels, tags, certificates, tx_proofs, blocks, broadcasts
9
+ - **Store** full BRC-100 action lifecycle: create, sign, promote, abort, reap, list, query
10
+ - **ProofStore** — merkle proof persistence with block normalization
11
+ - **UTXOPool** — UTXO selection with sizing strategy and limp mode
12
+ - **BroadcastQueue** — broadcast lifecycle management
13
+ - **Pushable/Fetchable** — Broadcast and Action adopt entity-driven network interaction
14
+ - **Migrations** — sequential schema migrations with constraint enforcement
15
+ - **Database trigger** — prevents outbound outputs from entering the spendable set
17
16
 
18
17
  ### Changed
18
+ - **blocks table** — normalized from tx_proofs; stores block headers independently
19
+ - **tx_reqs removed** — replaced by structural queries via Fetchable pattern
19
20
 
20
- - Minimum `bsv-wallet` version raised from `>= 0.6.0` to `>= 0.9.0`. Required for the new status taxonomy and `broadcast_enabled?` interface.
21
-
22
- ## 0.4.0 — 2026-04-12
23
-
24
- ### Added
25
- - `BSV::Wallet::SolidQueueAdapter` — PostgreSQL-backed async broadcast queue implementing the `BroadcastQueue` interface
26
- - Migration 006: `wallet_broadcast_jobs` table with `FOR UPDATE SKIP LOCKED` polling support
27
- - Background worker thread broadcasts transactions and promotes/rolls back wallet state
28
- - Recovery on restart via stale `locked_at` detection
29
- - Idempotent enqueue on duplicate txid (crash recovery)
30
- - `MAX_ATTEMPTS` enforcement (5) prevents infinite retry loops
31
- - Guard refuses MemoryStore attachment
32
-
33
- ### Fixed
34
- - Migration timestamps use `timestamptz` (matching migration 004 pattern)
35
- - `start()` check-and-set is atomic under mutex (prevents TOCTOU double-spawn)
36
- - Deserialization failures mark job as failed immediately (no tight retry loop)
37
-
38
- ## 0.3.1 — 2026-04-12
39
-
40
- ### Fixed
41
- - `update_action_status` now scopes to a single row by primary key, preventing unintended multi-row updates when duplicate txids exist
42
- - Added migration 005: unique index on `wallet_actions.txid` enforcing one action per transaction
43
-
44
- ## 0.3.0 — 2026-04-12
45
-
46
- ### Added
47
- - `update_action_status` and `delete_action` implementations for PostgresStore,
48
- matching the new StorageAdapter contract introduced in bsv-wallet 0.6.0 (#370)
49
-
50
- ## 0.2.0 — 2026-04-12
21
+ ## [0.1.0] - 2026-05-01
51
22
 
52
23
  ### Added
53
24
 
54
- - **Migration 004** adds `satoshis`, `pending_since`, `pending_reference`,
55
- `no_send` columns and a partial index on `(state, basket)` for spendable
56
- rows (#353)
57
- - **`find_spendable_outputs(basket:, min_satoshis:, sort_order:)`** — query
58
- spendable outputs with backward-compatible COALESCE for legacy rows (#354)
59
- - **`update_output_state(outpoint, new_state, ...)`** — transition output
60
- state with JSONB data synchronisation (#354)
61
- - **`lock_utxos(outpoints, reference:, no_send:)`** — atomic
62
- `UPDATE ... WHERE state = 'spendable' RETURNING` pattern for concurrent
63
- safety (#355)
64
- - **`release_stale_pending!(timeout:)`** — recover stuck pending outputs,
65
- exempting `no_send` locks (#355)
66
- - **PostgresStore settings methods** — `store_setting` / `find_setting`
67
-
68
- ### Fixed
69
-
70
- - **Spendable boolean sync** — `update_output_state`, `lock_utxos`, and
71
- `release_stale_pending!` now keep the legacy `spendable` column in sync
72
- with the `state` column; `filter_outputs` uses dual-column WHERE clause
73
-
74
- ### Changed
75
-
76
- - Directory restructure — source moved to `gem/bsv-wallet-postgres/`
77
-
78
- ## 0.1.0 — 2026-04-09
79
-
80
- Initial release of `bsv-wallet-postgres`, a PostgreSQL-backed
81
- `BSV::Wallet::StorageAdapter` implementation. Unblocks production
82
- deployments of `bsv-wallet` where state has to survive container
83
- restarts, and makes multi-instance wallet services possible for the
84
- first time.
85
-
86
- ### Added
87
-
88
- - **`BSV::Wallet::PostgresStore`** — full
89
- `StorageAdapter` implementation over Sequel + Postgres. Passes the
90
- same shared conformance suite that MemoryStore and FileStore pass
91
- (53 examples), plus 10 postgres-specific specs covering upsert
92
- semantics, GIN tag queries, JSONB attribute containment, concurrent
93
- inserts, and migration idempotency.
94
-
95
- - **Shipped Sequel migration** at
96
- `lib/bsv/wallet_postgres/migrations/001_create_wallet_tables.rb`.
97
- Five tables (wallet_outputs, wallet_actions, wallet_certificates,
98
- wallet_proofs, wallet_transactions) with JSONB data columns,
99
- dedicated indexed columns for filter paths, and GIN indexes on the
100
- `tags` / `labels` arrays.
101
-
102
- - **`PostgresStore.migrate!(db)`** convenience
103
- wrapper over `Sequel::Migrator.run` so consumers can apply the
104
- shipped schema with a single call. Operators who prefer their own
105
- migration framework can copy the migration file instead.
106
-
107
- - **Docs** at `docs/guides/wallet-postgres.md` with
108
- a 30-second quickstart, schema overview, and production
109
- considerations (pool sizing, multi-instance, backups,
110
- thread-safety).
111
-
112
- ### Infrastructure
113
-
114
- - **CI postgres service**. The GitHub Actions test job now runs a
115
- Postgres 16 container and exposes `DATABASE_URL` to rspec, so the
116
- `:postgres`-tagged specs run against a live database on every
117
- Ruby matrix row (2.7 → 3.4). Local developers without Postgres
118
- still get a green suite — those specs skip gracefully.
119
-
120
- ### Dependencies
121
-
122
- - `bsv-wallet-postgres` runtime: `bsv-wallet >= 0.3.4, < 1.0`,
123
- `sequel ~> 5`, `pg ~> 1`. The wallet floor matches the pinning style
124
- `bsv-wallet` uses for its `bsv-sdk` dependency so security releases
125
- propagate.
25
+ - Initial 17-table PostgreSQL schema migration
26
+ - Sequel models for all wallet tables
27
+ - Connection management module
data/LICENSE CHANGED
@@ -1,86 +1,29 @@
1
- Open BSV License Version 5 – granted by BSV Association, Grafenauweg 6, 6300
2
- Zug, Switzerland (CHE-427.008.338) ("Licensor"), to you as a user (henceforth
3
- "You", "User" or "Licensee").
1
+ Open BSV License
4
2
 
5
- For the purposes of this license, the definitions below have the following
6
- meanings:
3
+ Copyright (c) 2026 Simon Bettison
7
4
 
8
- "Bitcoin Protocol" means the protocol implementation, cryptographic rules,
9
- network protocols, and consensus mechanisms in the Bitcoin White Paper as
10
- described here https://protocol.bsvblockchain.org.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
11
 
12
- "Bitcoin White Paper" means the paper entitled 'Bitcoin: A Peer-to-Peer
13
- Electronic Cash System' published by 'Satoshi Nakamoto' in October 2008.
12
+ 1. The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
14
 
15
- "BSV Blockchains" means:
16
- (a) the Bitcoin blockchain containing block height #556767 with the hash
17
- "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and
18
- that contains the longest honest persistent chain of blocks which has been
19
- produced in a manner which is consistent with the rules set forth in the
20
- Network Access Rules; and
21
- (b) the test blockchains that contain the longest honest persistent chains of
22
- blocks which has been produced in a manner which is consistent with the
23
- rules set forth in the Network Access Rules.
24
-
25
- "Network Access Rules" or "Rules" means the set of rules regulating the
26
- relationship between BSV Association and the nodes on BSV based on the Bitcoin
27
- Protocol rules and those set out in the Bitcoin White Paper, and available here
28
- https://bsvblockchain.org/network-access-rules.
29
-
30
- "Software" means the software the subject of this licence, including any/all
31
- intellectual property rights therein and associated documentation files.
32
-
33
- BSV Association grants permission, free of charge and on a non-exclusive and
34
- revocable basis, to any person obtaining a copy of the Software to deal in the
35
- Software without restriction, including without limitation the rights to use,
36
- copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
37
- Software, and to permit persons to whom the Software is furnished to do so,
38
- subject to and conditioned upon the following conditions:
39
-
40
- 1 - The text "© BSV Association," and this license shall be included in all
41
- copies or substantial portions of the Software.
42
- 2 - The Software, and any software that is derived from the Software or parts
43
- thereof, must only be used on the BSV Blockchains.
44
-
45
- For the avoidance of doubt, this license is granted subject to and conditioned
46
- upon your compliance with these terms only. In the event of non-compliance, the
47
- license shall extinguish and you can be enjoined from violating BSV's
48
- intellectual property rights (incl. damages and similar related claims).
49
-
50
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES REGARDING ENTITLEMENT,
52
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
53
- EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS THEREOF BE LIABLE FOR ANY CLAIM,
54
- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
55
- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
56
- DEALINGS IN THE SOFTWARE.
57
-
58
-
59
- Version 0.1.1 of the Bitcoin SV software, and prior versions of software upon
60
- which it was based, were licensed under the MIT License, which is included below.
61
-
62
- The MIT License (MIT)
63
-
64
- Copyright (c) 2009-2010 Satoshi Nakamoto
65
- Copyright (c) 2009-2015 Bitcoin Developers
66
- Copyright (c) 2009-2017 The Bitcoin Core developers
67
- Copyright (c) 2017 The Bitcoin ABC developers
68
- Copyright (c) 2018 Bitcoin Association for BSV
69
- Copyright (c) 2023 BSV Association
70
-
71
- Permission is hereby granted, free of charge, to any person obtaining a copy of
72
- this software and associated documentation files (the "Software"), to deal in
73
- the Software without restriction, including without limitation the rights to
74
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
75
- the Software, and to permit persons to whom the Software is furnished to do so,
76
- subject to the following conditions:
77
-
78
- The above copyright notice and this permission notice shall be included in all
79
- copies or substantial portions of the Software.
15
+ 2. The Software, and any software that is derived from the Software or parts
16
+ thereof, can only be used on the Bitcoin SV blockchains. The Bitcoin SV
17
+ blockchains are defined, for purposes of this license, as the Bitcoin
18
+ blockchain containing block combinator
19
+ 000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b and the
20
+ test combinator
21
+ 00000000a6e140e27e4eb65e3cb4e4e07b1a0e3964b396c97a4b36e4ac06eb54.
80
22
 
81
23
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
82
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
83
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
84
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
85
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
86
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29
+ THE SOFTWARE.
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ extension :pg_enum
6
+
7
+ # Enum: broadcast intent for actions
8
+ create_enum(:broadcast_intent, %w[delayed inline none])
9
+
10
+ # 1. blocks — known block headers (chain tracker's local view)
11
+ create_table(:blocks) do
12
+ column :id, :bigint, primary_key: true, identity: :always
13
+ column :height, :integer, null: false, unique: true
14
+ column :merkle_root, :bytea, null: false
15
+ column :block_hash, :bytea
16
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
17
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
18
+ end
19
+
20
+ # 2. tx_proofs — merkle inclusion proofs (settlement evidence)
21
+ create_table(:tx_proofs) do
22
+ column :id, :bigint, primary_key: true, identity: :always
23
+ column :wtxid, :bytea, null: false, unique: true
24
+ foreign_key :block_id, :blocks, type: :bigint
25
+ column :block_index, :integer
26
+ column :merkle_path, :bytea
27
+ column :raw_tx, :bytea
28
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
29
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
30
+ end
31
+
32
+ # 2. actions — transaction lifecycle
33
+ create_table(:actions) do
34
+ column :id, :bigint, primary_key: true, identity: :always
35
+ foreign_key :tx_proof_id, :tx_proofs, type: :bigint
36
+ column :wtxid, :bytea
37
+ column :reference, :text, unique: true, default: Sequel.function(:gen_random_uuid)
38
+ column :outgoing, :boolean, null: false, default: true
39
+ column :satoshis, :bigint
40
+ column :description, :text
41
+ column :version, :integer
42
+ column :nlocktime, :bigint
43
+ column :broadcast, :broadcast_intent, null: false, default: 'delayed'
44
+ column :raw_tx, :bytea
45
+ column :input_beef, :bytea
46
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
47
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
48
+
49
+ index :wtxid, unique: true, where: Sequel.lit('wtxid IS NOT NULL')
50
+ index :broadcast
51
+ end
52
+
53
+ # 3. broadcasts — ARC lifecycle
54
+ create_table(:broadcasts) do
55
+ column :id, :bigint, primary_key: true, identity: :always
56
+ foreign_key :action_id, :actions, type: :bigint, null: false, unique: true
57
+ column :broadcast_at, :timestamptz
58
+ column :tx_status, :text
59
+ column :arc_status, :integer
60
+ column :block_hash, :bytea
61
+ column :block_height, :integer
62
+ column :merkle_path, :bytea
63
+ column :extra_info, :text
64
+ column :competing_txs, 'text[]'
65
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
66
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
67
+ end
68
+
69
+ # 4. baskets — output grouping with replenishment policy
70
+ create_table(:baskets) do
71
+ column :id, :bigint, primary_key: true, identity: :always
72
+ column :name, :text, null: false
73
+ column :target_count, :integer
74
+ column :target_value, :integer
75
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
76
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
77
+ column :deleted_at, :timestamptz
78
+
79
+ index :name, unique: true, where: Sequel.lit('deleted_at IS NULL')
80
+ end
81
+
82
+ # 5. outputs — immutable append-only log
83
+ create_table(:outputs) do
84
+ column :id, :bigint, primary_key: true, identity: :always
85
+ foreign_key :action_id, :actions, type: :bigint, null: false
86
+ column :satoshis, :bigint, null: false
87
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
88
+ column :locking_script, :bytea
89
+ column :vout, :integer, null: false
90
+ column :sender_identity_key, :text
91
+ column :derivation_prefix, :text
92
+ column :derivation_suffix, :text
93
+
94
+ unique %i[action_id vout]
95
+ end
96
+
97
+ # 6. spendable — the UTXO set (~28 bytes/row)
98
+ create_table(:spendable) do
99
+ column :id, :bigint, primary_key: true, identity: :always
100
+ foreign_key :output_id, :outputs, type: :bigint, null: false, unique: true
101
+ end
102
+
103
+ # 7. output_details — display and application metadata
104
+ create_table(:output_details) do
105
+ column :id, :bigint, primary_key: true, identity: :always
106
+ foreign_key :output_id, :outputs, type: :bigint, null: false, unique: true
107
+ column :change, :boolean, null: false, default: false
108
+ column :type, :text
109
+ column :purpose, :text
110
+ column :provided_by, :text
111
+ column :description, :text
112
+ column :custom_instructions, :text
113
+ column :script_length, :integer
114
+ column :script_offset, :integer
115
+ end
116
+
117
+ # 8. output_baskets — basket membership
118
+ create_table(:output_baskets) do
119
+ column :id, :bigint, primary_key: true, identity: :always
120
+ foreign_key :output_id, :outputs, type: :bigint, null: false, unique: true
121
+ foreign_key :basket_id, :baskets, type: :bigint, null: false
122
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
123
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
124
+
125
+ index :basket_id
126
+ end
127
+
128
+ # 9. inputs — structural lock mechanism
129
+ create_table(:inputs) do
130
+ column :id, :bigint, primary_key: true, identity: :always
131
+ foreign_key :action_id, :actions, type: :bigint, null: false, on_delete: :cascade
132
+ foreign_key :output_id, :outputs, type: :bigint, null: false
133
+ column :vin, :integer, null: false
134
+ column :nsequence, :bigint, null: false, default: 4_294_967_295
135
+ column :description, :text
136
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
137
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
138
+
139
+ unique :output_id
140
+ unique %i[action_id vin]
141
+ end
142
+
143
+ # 10. labels — label definitions
144
+ create_table(:labels) do
145
+ column :id, :bigint, primary_key: true, identity: :always
146
+ column :label, :text, null: false
147
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
148
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
149
+ column :deleted_at, :timestamptz
150
+
151
+ index :label, unique: true, where: Sequel.lit('deleted_at IS NULL')
152
+ end
153
+
154
+ # 11. action_labels — join table
155
+ create_table(:action_labels) do
156
+ column :id, :bigint, primary_key: true, identity: :always
157
+ foreign_key :action_id, :actions, type: :bigint, null: false
158
+ foreign_key :label_id, :labels, type: :bigint, null: false
159
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
160
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
161
+ column :deleted_at, :timestamptz
162
+
163
+ unique %i[action_id label_id]
164
+ index :label_id
165
+ end
166
+
167
+ # 12. tags — tag definitions
168
+ create_table(:tags) do
169
+ column :id, :bigint, primary_key: true, identity: :always
170
+ column :tag, :text, null: false
171
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
172
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
173
+ column :deleted_at, :timestamptz
174
+
175
+ index :tag, unique: true, where: Sequel.lit('deleted_at IS NULL')
176
+ end
177
+
178
+ # 13. output_tags — join table
179
+ create_table(:output_tags) do
180
+ column :id, :bigint, primary_key: true, identity: :always
181
+ foreign_key :output_id, :outputs, type: :bigint, null: false
182
+ foreign_key :tag_id, :tags, type: :bigint, null: false
183
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
184
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
185
+ column :deleted_at, :timestamptz
186
+
187
+ unique %i[output_id tag_id]
188
+ index :tag_id
189
+ end
190
+
191
+ # 14. certificates — identity certificates (BRC-52)
192
+ create_table(:certificates) do
193
+ column :id, :bigint, primary_key: true, identity: :always
194
+ column :type, :text, null: false
195
+ column :subject, :text
196
+ column :serial_number, :text, null: false
197
+ column :certifier, :text, null: false
198
+ column :verifier, :text
199
+ column :revocation_outpoint, :text
200
+ column :signature, :text
201
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
202
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
203
+ column :deleted_at, :timestamptz
204
+
205
+ unique %i[type serial_number certifier]
206
+ index :certifier
207
+ index :subject
208
+ end
209
+
210
+ # 15. certificate_fields — per-field encryption for selective revelation
211
+ create_table(:certificate_fields) do
212
+ column :id, :bigint, primary_key: true, identity: :always
213
+ foreign_key :certificate_id, :certificates, type: :bigint, null: false, on_delete: :cascade
214
+ column :name, :text, null: false
215
+ column :value, :text
216
+ column :master_key, :text
217
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
218
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
219
+
220
+ unique %i[certificate_id name]
221
+ end
222
+
223
+ # 16. tx_reqs — proof-harvesting work queue
224
+ create_table(:tx_reqs) do
225
+ column :id, :bigint, primary_key: true, identity: :always
226
+ foreign_key :tx_proof_id, :tx_proofs, type: :bigint
227
+ column :wtxid, :bytea, null: false, unique: true
228
+ column :status, :text, null: false, default: 'unmined'
229
+ column :attempts, :integer, null: false, default: 0
230
+ column :notified, :boolean, null: false, default: false
231
+ column :history, :text
232
+ column :notify, :text
233
+ column :batch, :text
234
+ column :raw_tx, :bytea
235
+ column :input_beef, :bytea
236
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
237
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
238
+
239
+ index :status
240
+ end
241
+
242
+ # 17. settings — key-value wallet configuration
243
+ create_table(:settings) do
244
+ column :id, :bigint, primary_key: true, identity: :always
245
+ column :key, :text, null: false, unique: true
246
+ column :value, :text
247
+ column :created_at, :timestamptz, null: false, default: Sequel.function(:now)
248
+ column :updated_at, :timestamptz, null: false, default: Sequel.function(:now)
249
+ end
250
+ end
251
+
252
+ down do
253
+ drop_table :settings, :tx_reqs, :certificate_fields, :certificates,
254
+ :output_tags, :tags, :action_labels, :labels, :inputs,
255
+ :output_baskets, :output_details, :spendable, :outputs,
256
+ :baskets, :broadcasts, :actions, :tx_proofs, :blocks
257
+
258
+ extension :pg_enum
259
+ drop_enum(:broadcast_intent)
260
+ end
261
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ # Add action_id with ON DELETE CASCADE to relationship tables.
6
+ # Denormalized (derivable via output_id -> outputs.action_id) but
7
+ # justified: set once at creation, never changes, enables cascade
8
+ # cleanup — deleting an action automatically removes its spendable
9
+ # entries, basket memberships, and output details.
10
+ add_column :spendable, :action_id, :bigint
11
+ run <<~SQL
12
+ ALTER TABLE spendable
13
+ ADD CONSTRAINT spendable_action_id_fkey
14
+ FOREIGN KEY (action_id) REFERENCES actions (id) ON DELETE CASCADE
15
+ SQL
16
+
17
+ add_column :output_baskets, :action_id, :bigint
18
+ run <<~SQL
19
+ ALTER TABLE output_baskets
20
+ ADD CONSTRAINT output_baskets_action_id_fkey
21
+ FOREIGN KEY (action_id) REFERENCES actions (id) ON DELETE CASCADE
22
+ SQL
23
+
24
+ add_column :output_details, :action_id, :bigint
25
+ run <<~SQL
26
+ ALTER TABLE output_details
27
+ ADD CONSTRAINT output_details_action_id_fkey
28
+ FOREIGN KEY (action_id) REFERENCES actions (id) ON DELETE CASCADE
29
+ SQL
30
+
31
+ # Change outputs.action_id FK to ON DELETE SET NULL and make nullable.
32
+ # Outputs are immutable log entries — when an action is deleted (abort,
33
+ # reaper), the output rows survive as orphans with NULL action_id.
34
+ # They have no spendable entry (cascade-deleted), no basket, and are
35
+ # invisible to the wallet.
36
+ run <<~SQL
37
+ ALTER TABLE outputs
38
+ DROP CONSTRAINT IF EXISTS outputs_action_id_fkey,
39
+ ALTER COLUMN action_id DROP NOT NULL,
40
+ ADD CONSTRAINT outputs_action_id_fkey
41
+ FOREIGN KEY (action_id) REFERENCES actions (id) ON DELETE SET NULL
42
+ SQL
43
+ end
44
+
45
+ down do
46
+ run <<~SQL
47
+ ALTER TABLE outputs
48
+ DROP CONSTRAINT IF EXISTS outputs_action_id_fkey,
49
+ ALTER COLUMN action_id SET NOT NULL,
50
+ ADD CONSTRAINT outputs_action_id_fkey
51
+ FOREIGN KEY (action_id) REFERENCES actions (id)
52
+ SQL
53
+ alter_table(:spendable) do
54
+ drop_constraint :spendable_action_id_fkey
55
+ drop_column :action_id
56
+ end
57
+ alter_table(:output_baskets) do
58
+ drop_constraint :output_baskets_action_id_fkey
59
+ drop_column :action_id
60
+ end
61
+ alter_table(:output_details) do
62
+ drop_constraint :output_details_action_id_fkey
63
+ drop_column :action_id
64
+ end
65
+ end
66
+ end