-
Notifications
You must be signed in to change notification settings - Fork 0
feat(gov-proposals): governance proposal creation (PR 8 backend) #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
793e0bc
b089dc3
ded0044
548d509
2e479f9
74f15e9
40dcb4e
fcd965e
960418c
481d758
6e5a0d9
4d65885
5919561
601eeed
56c4491
386108c
d3050d2
26be11d
748bf67
30b5866
5dd2098
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: ['**'] | ||
| pull_request: | ||
|
|
||
| concurrency: | ||
| group: ci-${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Jest (Node ${{ matrix.node }}) | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| node: ['20', '22'] | ||
|
|
||
| env: | ||
| CI: 'true' | ||
| # Keep tests hermetic: anything that auto-opts-in based on | ||
| # NODE_ENV stays in test mode, not dev/prod. | ||
| NODE_ENV: test | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node ${{ matrix.node }} | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: ${{ matrix.node }} | ||
| cache: npm | ||
|
|
||
| - name: Install | ||
| # `npm ci` is strict about package-lock.json; if the lock | ||
| # drifts we want the CI to fail loudly rather than silently | ||
| # resolve a new tree. | ||
| run: npm ci | ||
|
|
||
| - name: Run tests | ||
| run: npm test -- --runInBand --ci --colors | ||
|
|
||
| lint-sql: | ||
| name: Schema sanity | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Verify migrations directory has a deterministic ordering | ||
| # Guard against accidentally shipping two migrations with | ||
| # colliding numeric prefixes — the runner sorts lexicographically | ||
| # and duplicate prefixes would make apply order undefined. | ||
| run: | | ||
| set -euo pipefail | ||
| cd db/migrations | ||
| dup=$(ls | awk -F'_' '{print $1}' | sort | uniq -d || true) | ||
| if [ -n "$dup" ]; then | ||
| echo "Duplicate migration prefix(es): $dup" | ||
| exit 1 | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,41 @@ | |
| -- short-lived under Core's time window, and keeping them server-side | ||
| -- expands attack surface with zero replay value; a retry regenerates | ||
| -- a fresh sig client-side from the vault. | ||
| -- | ||
| -- proposal_drafts | ||
| -- User's in-progress proposal text. Server-side so the same draft | ||
| -- is available across devices once the user is logged in (Twitter- | ||
| -- compose-style: log out / close / switch devices and the drafts | ||
| -- follow the account). Drafts are plaintext because a governance | ||
| -- proposal's content is, by definition, about to go public on | ||
| -- chain — encrypting it would add friction for zero security | ||
| -- benefit. The payment_amount is stored in satoshis as INTEGER | ||
| -- (fits in int64 for every imaginable proposal size) to avoid the | ||
| -- float-precision traps of storing SYS decimals. | ||
| -- | ||
| -- proposal_submissions | ||
| -- One row per proposal the user has actually committed to publishing | ||
| -- (i.e. they've advanced past the draft step). The row is created at | ||
| -- "prepare" time with a frozen canonical snapshot (parent_hash + | ||
| -- revision + time_unix + data_hex + proposal_hash) — those fields | ||
| -- are the hash preimage and must not change after this point, else | ||
| -- the 150 SYS collateral OP_RETURN would stop matching. The row | ||
| -- moves through a small state machine advanced partly by the user | ||
| -- (reporting a collateral txid) and partly by the reminder-style | ||
| -- dispatcher (watching confirmations, calling gobject_submit once | ||
| -- mature). Statuses: | ||
| -- prepared hash + dataHex computed, shown to user, | ||
| -- no collateral yet. | ||
| -- awaiting_collateral user has supplied a collateral txid; | ||
| -- dispatcher is polling confirmations. | ||
| -- submitted gobject_submit succeeded; governance_hash | ||
| -- is set. Terminal (happy path). | ||
| -- failed something fatal happened; fail_reason | ||
| -- is a stable machine code, fail_detail is | ||
| -- raw context. Terminal. | ||
| -- There is no 'abandoned' status — users who back out before paying | ||
| -- just DELETE their row. The status column has no CHECK constraint | ||
| -- so the repo layer owns validation (mirroring vote_receipts.status). | ||
|
|
||
| CREATE TABLE users ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
|
|
@@ -191,3 +226,108 @@ CREATE INDEX idx_receipts_user_proposal | |
| ON vote_receipts(user_id, proposal_hash); | ||
| CREATE INDEX idx_receipts_user_recent | ||
| ON vote_receipts(user_id, submitted_at DESC); | ||
|
|
||
| -- proposal_drafts: user's in-progress proposal content. No canonical | ||
| -- snapshot or hash here — drafts haven't committed to an on-chain | ||
| -- identity yet. `payment_amount_sats` is an integer number of | ||
| -- satoshis (int64 range easily accommodates any realistic amount); | ||
| -- storing SYS as a decimal REAL would drift under float arithmetic. | ||
| -- `start_epoch` / `end_epoch` are nullable because a user may save | ||
| -- before choosing a superblock. | ||
| CREATE TABLE proposal_drafts ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
|
Comment on lines
+237
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These schema changes were appended to Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N/A
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged, but this is a false positive relative to this project's explicit pre-launch convention. See the header of
Nothing is deployed yet. The Keeping the review signal tight: marking this as "does not apply in this iteration". The new partial unique index ( |
||
| title TEXT NOT NULL DEFAULT '', | ||
| name TEXT NOT NULL DEFAULT '', | ||
| url TEXT NOT NULL DEFAULT '', | ||
| description TEXT NOT NULL DEFAULT '', | ||
| payment_address TEXT NOT NULL DEFAULT '', | ||
| payment_amount_sats INTEGER NOT NULL DEFAULT 0, | ||
| payment_count INTEGER NOT NULL DEFAULT 1, | ||
| start_epoch INTEGER, | ||
| end_epoch INTEGER, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL | ||
| ); | ||
|
|
||
| CREATE INDEX idx_proposal_drafts_user_recent | ||
| ON proposal_drafts(user_id, updated_at DESC); | ||
|
|
||
| -- proposal_submissions: once the user commits to publishing, we | ||
| -- snapshot the canonical (parent_hash, revision, time_unix, data_hex, | ||
| -- proposal_hash) tuple. Anything derived from data_hex (name, url, | ||
| -- payment_*) is duplicated in typed columns for indexing and display, | ||
| -- but the source of truth for what the chain sees is data_hex — the | ||
| -- repo layer guarantees the denormalized columns stay in sync with | ||
| -- it. draft_id is intentionally ON DELETE SET NULL so a user can | ||
| -- clean up their drafts list without destroying the historical | ||
| -- record of what they submitted. | ||
| CREATE TABLE proposal_submissions ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| draft_id INTEGER REFERENCES proposal_drafts(id) ON DELETE SET NULL, | ||
|
|
||
| parent_hash TEXT NOT NULL DEFAULT '0', | ||
| revision INTEGER NOT NULL DEFAULT 1, | ||
| time_unix INTEGER NOT NULL, | ||
| data_hex TEXT NOT NULL, | ||
| proposal_hash TEXT NOT NULL, | ||
|
|
||
| title TEXT NOT NULL DEFAULT '', | ||
| name TEXT NOT NULL, | ||
| url TEXT NOT NULL, | ||
| payment_address TEXT NOT NULL, | ||
| payment_amount_sats INTEGER NOT NULL, | ||
| payment_count INTEGER NOT NULL DEFAULT 1, | ||
| start_epoch INTEGER NOT NULL, | ||
| end_epoch INTEGER NOT NULL, | ||
|
|
||
| status TEXT NOT NULL, | ||
| collateral_txid TEXT, | ||
| collateral_confs INTEGER NOT NULL DEFAULT 0, | ||
| governance_hash TEXT, | ||
| fail_reason TEXT, | ||
| fail_detail TEXT, | ||
|
|
||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL | ||
| ); | ||
|
|
||
| -- Per-user recency index (for the "your submissions" page). | ||
| CREATE INDEX idx_proposal_submissions_user_recent | ||
| ON proposal_submissions(user_id, updated_at DESC); | ||
|
|
||
| -- Dispatcher-facing index: the watcher tick scans rows by status to | ||
| -- advance them, so keep that lookup fast regardless of table size. | ||
| CREATE INDEX idx_proposal_submissions_status | ||
| ON proposal_submissions(status, updated_at); | ||
|
|
||
| -- Partial uniqueness on collateral_txid: a given collateral tx can | ||
| -- only back a single proposal submission. Two rows claiming the same | ||
| -- txid is a bug (probably a duplicate "I paid, here's the txid" call | ||
| -- from the user). NULL txids are exempt, which is the correct | ||
| -- treatment for rows still in `prepared` state. | ||
| CREATE UNIQUE INDEX idx_proposal_submissions_collateral_txid | ||
| ON proposal_submissions(collateral_txid) | ||
| WHERE collateral_txid IS NOT NULL; | ||
|
|
||
| -- Codex PR8 round 3 P2: enforce /prepare idempotency at the DB layer. | ||
| -- The route reads by (user_id, data_hex, status='prepared') and then | ||
| -- inserts; without this partial unique index, two concurrent requests | ||
| -- with identical payload can both miss the read and both insert, | ||
| -- producing duplicate `prepared` rows for the same logical proposal. | ||
| -- Once the row moves past `prepared` (the user attaches collateral, | ||
| -- or it ends up `submitted`/`failed`), the partial predicate no | ||
| -- longer matches and a subsequent retry with the same dataHex is | ||
| -- free to create a fresh `prepared` row — which is the correct UX: | ||
| -- the old submission is locked to a specific collateral txid, and a | ||
| -- re-prepare is the user explicitly asking for a clean second take. | ||
| CREATE UNIQUE INDEX idx_proposal_submissions_user_payload_prepared | ||
| ON proposal_submissions(user_id, data_hex) | ||
| WHERE status = 'prepared'; | ||
|
|
||
| -- Governance hash is likewise unique once set — it IS the proposal's | ||
| -- on-chain identity. A NULL is expected for rows not yet submitted. | ||
| CREATE UNIQUE INDEX idx_proposal_submissions_governance_hash | ||
| ON proposal_submissions(governance_hash) | ||
| WHERE governance_hash IS NOT NULL; | ||
Uh oh!
There was an error while loading. Please reload this page.