feat(voting): frontend registration, WYSIWYG proposals (≥1000 CL8Y), and signed voting UX #511

Closed
opened 2026-08-09 08:25:59 +00:00 by PlasticDigits · 10 comments
PlasticDigits commented 2026-08-09 08:25:59 +00:00 (Migrated from gitlab.com)

Summary

Ship the dApp UX for offchain CL8Y snapshot voting: wallet registration (sign message), WYSIWYG proposal creation (requires ≥1000 CL8Y + signature), and voting — talking only to operator-voting + existing LCD/wallet stack. No smart contracts.

Depends on:

  • Indexer registration + CL8Y ledger for registered wallets
  • operator-voting APIs (signatures, proposals, votes, blacklist enforcement server-side)

CL8Y: hpax3 / VITE_CL8Y_TOKEN_ADDRESS (same asset as fee-discount tiers).


Current codebase

Area Status
Wallet connect frontend-dapp/src/services/terraclassic/wallet.ts — Keplr / Station / WC / Simulated Wallet
Tx signing terraWalletSignTxRaw.ts, terraBroadcast.ts — Amino/Direct for txs only
Arbitrary message sign Not implemented; vite-env.d.ts lacks signArbitrary
CL8Y balance display helpers getTokenBalance in queries.ts; tiers UX on fee-discount pages
Routing / nav App.tsx, components/common/navItems.ts — no voting routes
Design system docs/design-system.md, skills AGENTS_FRONTEND_DESIGN_SYSTEM.md, AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md
Indexer client usage Frontend already consumes indexer HTTP for charts/routes — pattern for a second base URL (VITE_OPERATOR_VOTING_URL)
WYSIWYG None in-repo today

Why this is needed

Operators and tokenholders need a browser flow to opt into the electorate, publish human-readable proposals, and cast signed votes weighted by registered CL8Y balances — without governance multisig or on-chain poll contracts. The UI must make registration before proposal creation obvious, show eligibility (≥1000 CL8Y to propose), and fail clearly when blacklisted or unregistered.


Constraints / guardrails

  1. No smart contracts and no direct Postgres from the browser.
  2. All mutations go through operator-voting; frontend never trusts client-side blacklist alone (server enforces VOTING_BLACKLIST_ADDRESSES).
  3. Registration required before a wallet is in the snapshot electorate; copy must say users should register before proposals they care about are created.
  4. Proposal create: WYSIWYG in browser → canonicalize content → wallet signature → POST with sig; disable/submit gate if balance < 1000 CL8Y (display from LCD and/or voting API; server is source of truth).
  5. Use design-system / retail copy skills — low cognitive load; do not imply on-chain governance finality if votes are advisory/offchain.
  6. Keplr signArbitrary (ADR-36) primary; Simulated Wallet must work on LocalTerra for QA; document Station/WC support matrix (degrade gracefully if arbitrary sign unsupported).
  7. Sanitize render of proposal HTML (dangerouslySetInnerHTML only with server-sanitized allowlist output, or re-sanitize client-side with same policy).
  8. Env: VITE_OPERATOR_VOTING_URL, existing VITE_CL8Y_TOKEN_ADDRESS; never embed DB URLs.
  9. Blacklisted users: clear error on vote/propose attempts.
  10. Follow existing Layout/nav patterns; new route e.g. /vote or /governance (product pick — avoid clashing with on-chain “governance” multisig docs; prefer /vote).

Relevant files

  • frontend-dapp/src/App.tsx, components/common/Layout.tsx, navItems.ts, navItems.test.ts
  • frontend-dapp/src/services/terraclassic/wallet.ts, terraWalletSignTxRaw.ts, queries.ts, vite-env.d.ts
  • frontend-dapp/src/utils/constants.ts, tokenRegistry.ts
  • New: pages/components for register / proposal list / proposal detail / create (WYSIWYG) / cast vote
  • New: services/operatorVoting.ts (API client + sign payload helpers)
  • frontend-dapp/.env*, Coolify frontend env examples
  • Skills: AGENTS_FRONTEND_DESIGN_SYSTEM.md, AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md, AGENTS_FRONTEND_THEME_TOGGLE.md

Routes

  • /vote — list proposals + registration CTA
  • /vote/new — WYSIWYG create (wallet connected, ≥1000 CL8Y gate, sign + submit)
  • /vote/:id — render proposal, snapshot info, cast vote, show results

Registration

  1. Connected wallet → “Register for voting” → signArbitrary over canonical register payload → POST /v1/register.
  2. Show registered height / status; explain that balances are tracked from registration forward.

Proposal create

  1. WYSIWYG editor (choose a maintained editor compatible with XSS allowlist — e.g. TipTap/Lexical with strict schema).
  2. Preview + raw canonicalization for signing.
  3. Gate: connected + registered (if required by API) + CL8Y ≥ 1000.
  4. Sign proposal payload; submit; on success navigate to detail.

Vote

  1. Choices per proposal schema (for/against/abstain or options from API).
  2. Sign vote payload; submit; show weight from snapshot (not confusingly live balance after).
  3. Handle blacklist / already voted / unregistered errors with plain-language copy.

QA

  • Simulated Wallet path for LocalTerra Playwright/unit tests with mocked operator-voting and/or full stack.

Acceptance criteria

  • Nav + routes for voting flows; pages match design system.
  • User can register by signing a message; status visible.
  • User with ≥1000 CL8Y can create a proposal via WYSIWYG + signature; below threshold cannot.
  • User can cast a signed vote on an open proposal and see tally/weight.
  • Unregistered / blacklisted / unsupported-wallet-sign paths show clear errors.
  • VITE_OPERATOR_VOTING_URL documented; Coolify frontend env updated in examples.
  • Unit tests for gates and API client; Playwright or component tests for critical paths.
  • No contract changes; no direct DB access from frontend.

Test plan (all paths)

  1. Unit — gates: propose button disabled <1000 CL8Y; enabled at boundary.
  2. Unit — API client: payload canonicalization stable for signing.
  3. Component — register / vote / create: success and error toasts/banners.
  4. Simulated wallet: signArbitrary (or local equivalent) produces server-acceptable sig against LocalTerra operator-voting.
  5. Keplr manual QA: register, create, vote on LocalTerra/staging.
  6. Render safety: proposal containing <script> / onerror= does not execute.
  7. Disconnected wallet: CTAs to connect; no panic.
  8. Nav/a11y: routes in navItems tests; basic keyboard focus on editor/actions.
  9. E2E (optional strict): full register → propose → vote against LocalTerra stack.

Test plan — attack, hack & abuse vectors

  1. Client-only gate bypass: craft API calls without UI — server must still enforce threshold, sig, blacklist (frontend tests document expected API errors).
  2. XSS via proposal HTML: malicious WYSIWYG content; verify sanitized render.
  3. Phishing copy: UI must not ask for seed/mnemonic; only wallet popup sign.
  4. Wrong network / chain-id: signing domain mismatch → readable error.
  5. Replay: UI should not resubmit identical vote blindly; handle 409 from API.
  6. Blacklist UX: denylisted wallet cannot vote; no alternate client path.
  7. Balance spoof: tampering displayed balance in DevTools must not affect server accept/reject.

Verification criteria

  • make test-frontend / targeted vitest for new modules pass.
  • Local manual or Playwright: register → create (≥1000 CL8Y) → vote.
  • Staging Coolify: env present; pages reachable behind CORS to operator-voting.
  • Copy reviewed against cognitive-load skill (advisory offchain poll vs on-chain gov).
  • Linked backend issues verified first or stubbed with MSW for UI-only progress.

Labels

frontend UX enhancement security product

Priority

P1

## Summary Ship the **dApp UX** for offchain CL8Y snapshot voting: wallet **registration** (sign message), **WYSIWYG proposal creation** (requires ≥1000 CL8Y + signature), and **voting** — talking only to **`operator-voting`** + existing LCD/wallet stack. **No smart contracts.** Depends on: - Indexer registration + CL8Y ledger for registered wallets - `operator-voting` APIs (signatures, proposals, votes, blacklist enforcement server-side) **CL8Y:** hpax3 / `VITE_CL8Y_TOKEN_ADDRESS` (same asset as fee-discount tiers). --- ## Current codebase | Area | Status | |------|--------| | Wallet connect | `frontend-dapp/src/services/terraclassic/wallet.ts` — Keplr / Station / WC / Simulated Wallet | | Tx signing | `terraWalletSignTxRaw.ts`, `terraBroadcast.ts` — Amino/Direct for txs only | | Arbitrary message sign | **Not implemented**; `vite-env.d.ts` lacks `signArbitrary` | | CL8Y balance display helpers | `getTokenBalance` in `queries.ts`; tiers UX on fee-discount pages | | Routing / nav | `App.tsx`, `components/common/navItems.ts` — no voting routes | | Design system | `docs/design-system.md`, skills `AGENTS_FRONTEND_DESIGN_SYSTEM.md`, `AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md` | | Indexer client usage | Frontend already consumes indexer HTTP for charts/routes — pattern for a second base URL (`VITE_OPERATOR_VOTING_URL`) | | WYSIWYG | **None** in-repo today | --- ## Why this is needed Operators and tokenholders need a browser flow to opt into the electorate, publish human-readable proposals, and cast signed votes weighted by registered CL8Y balances — without governance multisig or on-chain poll contracts. The UI must make registration **before** proposal creation obvious, show eligibility (≥1000 CL8Y to propose), and fail clearly when blacklisted or unregistered. --- ## Constraints / guardrails 1. **No smart contracts** and no direct Postgres from the browser. 2. All mutations go through **`operator-voting`**; frontend never trusts client-side blacklist alone (server enforces `VOTING_BLACKLIST_ADDRESSES`). 3. Registration **required before** a wallet is in the snapshot electorate; copy must say users should register before proposals they care about are created. 4. Proposal create: WYSIWYG in browser → canonicalize content → **wallet signature** → POST with sig; disable/submit gate if balance &lt; 1000 CL8Y (display from LCD and/or voting API; server is source of truth). 5. Use design-system / retail copy skills — low cognitive load; do not imply on-chain governance finality if votes are advisory/offchain. 6. Keplr `signArbitrary` (ADR-36) primary; Simulated Wallet must work on LocalTerra for QA; document Station/WC support matrix (degrade gracefully if arbitrary sign unsupported). 7. Sanitize render of proposal HTML (dangerouslySetInnerHTML only with server-sanitized allowlist output, or re-sanitize client-side with same policy). 8. Env: `VITE_OPERATOR_VOTING_URL`, existing `VITE_CL8Y_TOKEN_ADDRESS`; never embed DB URLs. 9. Blacklisted users: clear error on vote/propose attempts. 10. Follow existing Layout/nav patterns; new route e.g. `/vote` or `/governance` (product pick — avoid clashing with on-chain “governance” multisig docs; prefer **`/vote`**). --- ## Relevant files - `frontend-dapp/src/App.tsx`, `components/common/Layout.tsx`, `navItems.ts`, `navItems.test.ts` - `frontend-dapp/src/services/terraclassic/wallet.ts`, `terraWalletSignTxRaw.ts`, `queries.ts`, `vite-env.d.ts` - `frontend-dapp/src/utils/constants.ts`, `tokenRegistry.ts` - New: pages/components for register / proposal list / proposal detail / create (WYSIWYG) / cast vote - New: `services/operatorVoting.ts` (API client + sign payload helpers) - `frontend-dapp/.env*`, Coolify frontend env examples - Skills: `AGENTS_FRONTEND_DESIGN_SYSTEM.md`, `AGENTS_FRONTEND_COPY_COGNITIVE_LOAD.md`, `AGENTS_FRONTEND_THEME_TOGGLE.md` --- ## Recommended direction ### Routes - `/vote` — list proposals + registration CTA - `/vote/new` — WYSIWYG create (wallet connected, ≥1000 CL8Y gate, sign + submit) - `/vote/:id` — render proposal, snapshot info, cast vote, show results ### Registration 1. Connected wallet → “Register for voting” → `signArbitrary` over canonical register payload → `POST /v1/register`. 2. Show registered height / status; explain that balances are tracked from registration forward. ### Proposal create 1. WYSIWYG editor (choose a maintained editor compatible with XSS allowlist — e.g. TipTap/Lexical with strict schema). 2. Preview + raw canonicalization for signing. 3. Gate: connected + registered (if required by API) + CL8Y ≥ 1000. 4. Sign proposal payload; submit; on success navigate to detail. ### Vote 1. Choices per proposal schema (for/against/abstain or options from API). 2. Sign vote payload; submit; show weight from snapshot (not confusingly live balance after). 3. Handle blacklist / already voted / unregistered errors with plain-language copy. ### QA - Simulated Wallet path for LocalTerra Playwright/unit tests with mocked `operator-voting` and/or full stack. --- ## Acceptance criteria - [ ] Nav + routes for voting flows; pages match design system. - [ ] User can register by signing a message; status visible. - [ ] User with ≥1000 CL8Y can create a proposal via WYSIWYG + signature; below threshold cannot. - [ ] User can cast a signed vote on an open proposal and see tally/weight. - [ ] Unregistered / blacklisted / unsupported-wallet-sign paths show clear errors. - [ ] `VITE_OPERATOR_VOTING_URL` documented; Coolify frontend env updated in examples. - [ ] Unit tests for gates and API client; Playwright or component tests for critical paths. - [ ] No contract changes; no direct DB access from frontend. --- ## Test plan (all paths) 1. **Unit — gates:** propose button disabled &lt;1000 CL8Y; enabled at boundary. 2. **Unit — API client:** payload canonicalization stable for signing. 3. **Component — register / vote / create:** success and error toasts/banners. 4. **Simulated wallet:** signArbitrary (or local equivalent) produces server-acceptable sig against LocalTerra `operator-voting`. 5. **Keplr manual QA:** register, create, vote on LocalTerra/staging. 6. **Render safety:** proposal containing `<script>` / `onerror=` does not execute. 7. **Disconnected wallet:** CTAs to connect; no panic. 8. **Nav/a11y:** routes in `navItems` tests; basic keyboard focus on editor/actions. 9. **E2E (optional strict):** full register → propose → vote against LocalTerra stack. --- ## Test plan — attack, hack & abuse vectors 1. **Client-only gate bypass:** craft API calls without UI — server must still enforce threshold, sig, blacklist (frontend tests document expected API errors). 2. **XSS via proposal HTML:** malicious WYSIWYG content; verify sanitized render. 3. **Phishing copy:** UI must not ask for seed/mnemonic; only wallet popup sign. 4. **Wrong network / chain-id:** signing domain mismatch → readable error. 5. **Replay:** UI should not resubmit identical vote blindly; handle 409 from API. 6. **Blacklist UX:** denylisted wallet cannot vote; no alternate client path. 7. **Balance spoof:** tampering displayed balance in DevTools must not affect server accept/reject. --- ## Verification criteria - [ ] `make test-frontend` / targeted vitest for new modules pass. - [ ] Local manual or Playwright: register → create (≥1000 CL8Y) → vote. - [ ] Staging Coolify: env present; pages reachable behind CORS to `operator-voting`. - [ ] Copy reviewed against cognitive-load skill (advisory offchain poll vs on-chain gov). - [ ] Linked backend issues verified first or stubbed with MSW for UI-only progress. ## Labels `frontend` `UX` `enhancement` `security` `product` ## Priority P1
PlasticDigits commented 2026-08-09 08:26:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #509

mentioned in issue #509
PlasticDigits commented 2026-08-09 08:26:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #510

mentioned in issue #510
PlasticDigits commented 2026-08-09 08:26:08 +00:00 (Migrated from gitlab.com)
Issue Role
#509 Indexer registration + CL8Y ledger
#510 operator-voting API (frontend talks only to this + LCD/wallet)
#511 (this) dApp WYSIWYG proposals, register, vote

Blocked by #510 for real API; #509 required for meaningful snapshot weights.

## Bundle links | Issue | Role | |-------|------| | **#509** | Indexer registration + CL8Y ledger | | **#510** | `operator-voting` API (frontend talks only to this + LCD/wallet) | | **#511** (this) | dApp WYSIWYG proposals, register, vote | Blocked by #510 for real API; #509 required for meaningful snapshot weights.
PlasticDigits commented 2026-08-09 08:26:17 +00:00 (Migrated from gitlab.com)

marked as related to #509

marked as related to #509
PlasticDigits commented 2026-08-09 08:26:18 +00:00 (Migrated from gitlab.com)

marked as related to #510

marked as related to #510
PlasticDigits commented 2026-08-21 11:32:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #588

mentioned in issue #588
PlasticDigits commented 2026-08-21 11:32:38 +00:00 (Migrated from gitlab.com)

marked as related to #588

marked as related to #588
PlasticDigits commented 2026-08-21 11:32:51 +00:00 (Migrated from gitlab.com)

Scope add: EVM wallets on /vote (core)

A majority of CL8Y holders are on BSC, not Terra Classic. The dApp must let users connect an EVM wallet (injected EIP-1193 / WalletConnect eip155:56) and complete register → propose (≥1000 BEP-20 CL8Y) → vote with personal_sign.

Do not ship a Keplr-only /vote as done. Do not break Cosmos WalletConnect mobile pairing (#519 / #554 / #566). Balance reads stay on the indexer — no VITE_* BSC RPC (same hygiene as #571).

Tracking issue (BSC integration): https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/588

Related Role
#509 Terra CW20 and BSC BEP-20 ledgers
#510 ADR-36 and EIP-191 APIs
#511 (this) Terra and EVM /vote UX
#588 BSC integration contract / guardrails
## Scope add: EVM wallets on `/vote` (core) A **majority of CL8Y holders are on BSC**, not Terra Classic. The dApp must let users **connect an EVM wallet** (injected EIP-1193 / WalletConnect `eip155:56`) and complete **register → propose (≥1000 BEP-20 CL8Y) → vote** with `personal_sign`. Do not ship a Keplr-only `/vote` as done. Do not break Cosmos WalletConnect mobile pairing (#519 / #554 / #566). Balance reads stay on the indexer — no `VITE_*` BSC RPC (same hygiene as #571). **Tracking issue (BSC integration):** https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/588 | Related | Role | |---------|------| | **#509** | Terra CW20 **and** BSC BEP-20 ledgers | | **#510** | ADR-36 **and** EIP-191 APIs | | **#511** (this) | Terra **and** EVM `/vote` UX | | **#588** | BSC integration contract / guardrails |
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-25 06:07:27 +00:00
PlasticDigits commented 2026-08-25 06:08:50 +00:00 (Migrated from gitlab.com)

Voting work left this repo on 2026-08-25.

Canonical project: https://gitlab.com/PlasticDigits/voting

Former IID here New
#509 https://gitlab.com/PlasticDigits/voting/-/issues/1
#510 https://gitlab.com/PlasticDigits/voting/-/issues/2
#511 https://gitlab.com/PlasticDigits/voting/-/issues/3
#588 https://gitlab.com/PlasticDigits/voting/-/issues/4

Also opened there: Legal clickwrap (#5), wallet reuse (#6). Do not implement voting in this DEX repo.

Voting work left this repo on 2026-08-25. Canonical project: https://gitlab.com/PlasticDigits/voting | Former IID here | New | |-----------------|-----| | #509 | https://gitlab.com/PlasticDigits/voting/-/issues/1 | | #510 | https://gitlab.com/PlasticDigits/voting/-/issues/2 | | #511 | https://gitlab.com/PlasticDigits/voting/-/issues/3 | | #588 | https://gitlab.com/PlasticDigits/voting/-/issues/4 | Also opened there: Legal clickwrap (#5), wallet reuse (#6). Do not implement voting in this DEX repo.
PlasticDigits commented 2026-08-25 06:08:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #637

mentioned in issue #637
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#511
No description provided.