From 969f3f2dc40f37ec8bc1923dc3a52c5c718099f2 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 6 Jul 2026 02:42:54 +0000 Subject: [PATCH 1/2] feat: add hat stewards dashboard --- .env.example | 11 + README.md | 7 + drizzle/0020_medical_mysterio.sql | 16 + drizzle/meta/0020_snapshot.json | 3804 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/hat-stewards/actions.ts | 240 ++ .../hat-stewards/hat-steward-admin-form.tsx | 280 ++ src/app/hat-stewards/page.tsx | 358 ++ src/components/app-header.tsx | 1 + src/db/schema.ts | 23 + src/lib/hat-stewards/hats.ts | 319 ++ src/lib/hat-stewards/role-safes.ts | 80 + src/lib/hat-stewards/streams.ts | 300 ++ tests/e2e/responsive-dashboard.spec.ts | 2 + 14 files changed, 5448 insertions(+) create mode 100644 drizzle/0020_medical_mysterio.sql create mode 100644 drizzle/meta/0020_snapshot.json create mode 100644 src/app/hat-stewards/actions.ts create mode 100644 src/app/hat-stewards/hat-steward-admin-form.tsx create mode 100644 src/app/hat-stewards/page.tsx create mode 100644 src/lib/hat-stewards/hats.ts create mode 100644 src/lib/hat-stewards/role-safes.ts create mode 100644 src/lib/hat-stewards/streams.ts diff --git a/.env.example b/.env.example index bef4fef..0628d72 100644 --- a/.env.example +++ b/.env.example @@ -29,6 +29,15 @@ DAO_SHARE_CHAIN_ID= HATS_CONTRACT_ADDRESS= # Decimal or hex Hat ID values are both accepted. ANGRY_DWARF_HAT_ID= +# Optional; used by /hat-stewards admin controls to fetch selectable Hats. +HATS_SUBGRAPH_URL= +# Optional; sent as Authorization: Bearer ... for private or API-keyed subgraphs. +HATS_SUBGRAPH_AUTH_TOKEN= +HAT_STEWARDS_TREE_ID=92 +HAT_STEWARDS_CHAIN_ID=100 +HATS_APP_BASE_URL=https://app.hatsprotocol.xyz +# Optional; defaults to https://ipfs.io/ipfs for Hat metadata names. +HATS_IPFS_GATEWAY_URL=https://ipfs.io/ipfs # Machine API / x402 demo configuration # Defaults to Base Sepolia chain ID 84532. @@ -50,6 +59,8 @@ DAOHAUS_SUBGRAPH_URL= DAOHAUS_APP_BASE_URL= # Optional; defaults to https://safe-transaction-gnosis-chain.safe.global/api/v1. SAFE_TRANSACTION_SERVICE_URL= +# Optional; defaults to the public Superfluid Gnosis subgraph. +SUPERFLUID_SUBGRAPH_URL=https://subgraph-endpoints.superfluid.dev/xdai-mainnet/protocol-v1 # Optional; default block lookback when syncing operator transfers outside a specific quarter. OPERATOR_TRANSFER_LOG_BLOCK_LOOKBACK=50000 # Optional; lowers or raises ERC-20 log query chunk size for operator transfer sync. diff --git a/README.md b/README.md index 862917a..2ec727b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ allocation, account breakdowns, sync freshness, and stablecoin rebalancing guidance. Stablecoin allocation targets 25% stables and 75% other assets, with a 5 percentage point tolerance. +Hat Steward views show mapped role Safes, link each Safe to its Hats Protocol +role, and display active incoming Superfluid streams projected by quarter. + ## Architecture - Next.js App Router renders the wallet-gated dashboard. @@ -25,6 +28,8 @@ guidance. Stablecoin allocation targets 25% stables and 75% other assets, with a quarter workflow state, audit records, and entity/address book records. - Treasury balance snapshots are synced from environment-configured accounts and cached for member-visible reads. +- Hat Steward role Safe mappings are stored in Postgres. Admins choose Hats from + the configured Hats tree and enter the Safe address for each steward role. - Published quarter reports and exports are member-visible. Draft, ready-for-review, reopened, and administrative accounting workflows remain restricted to the appropriate roles. @@ -145,6 +150,8 @@ The app targets Neon in production. At runtime it uses Neon HTTP for Neon URLs a RaidGuild member access is checked with `DAO_SHARE_TOKEN_ADDRESS`, the DAOhaus/Baal ERC-20 shares token. `DAO_SHARE_THRESHOLD` is written as a human share amount such as `100`. `HATS_CONTRACT_ADDRESS` is the Hats Protocol contract address used for hats-based permissions, formatted as a `0x`-prefixed EVM address. Use the deployed Hats contract for the target network, or a local test contract address for local chain testing. `ANGRY_DWARF_HAT_ID` can be provided as a decimal or hex string and requires `HATS_CONTRACT_ADDRESS` to function. +`HATS_SUBGRAPH_URL` enables the Hat Stewards admin picker. `HATS_SUBGRAPH_AUTH_TOKEN` is optional and is sent as an `Authorization: Bearer ...` header for private or API-keyed subgraphs. `HAT_STEWARDS_TREE_ID` defaults to `92` for the RaidGuild tree, and `HAT_STEWARDS_CHAIN_ID` defaults to `100` for Gnosis. `HATS_IPFS_GATEWAY_URL` defaults to `https://ipfs.io/ipfs` and is used to resolve Hat metadata names for the admin picker. +`SUPERFLUID_SUBGRAPH_URL` defaults to Superfluid's public Gnosis endpoint and powers Hat Steward incoming stream totals. `ENCRYPTION_KEY` must be a base64-encoded 32-byte key. Multiple-key rotation requires stable `key-id:base64-key` entries. To generate a local development key: diff --git a/drizzle/0020_medical_mysterio.sql b/drizzle/0020_medical_mysterio.sql new file mode 100644 index 0000000..5810347 --- /dev/null +++ b/drizzle/0020_medical_mysterio.sql @@ -0,0 +1,16 @@ +CREATE TABLE "hat_steward_role_safes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "hat_id" text NOT NULL, + "hat_pretty_id" text NOT NULL, + "hat_label" text NOT NULL, + "safe_address" text NOT NULL, + "chain_id" integer DEFAULT 100 NOT NULL, + "notes" text, + "archived_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX "hat_steward_role_safes_hat_id_unique" ON "hat_steward_role_safes" USING btree ("hat_id");--> statement-breakpoint +CREATE INDEX "hat_steward_role_safes_safe_idx" ON "hat_steward_role_safes" USING btree ("chain_id",lower("safe_address"));--> statement-breakpoint +CREATE INDEX "hat_steward_role_safes_archived_at_idx" ON "hat_steward_role_safes" USING btree ("archived_at"); \ No newline at end of file diff --git a/drizzle/meta/0020_snapshot.json b/drizzle/meta/0020_snapshot.json new file mode 100644 index 0000000..6e60887 --- /dev/null +++ b/drizzle/meta/0020_snapshot.json @@ -0,0 +1,3804 @@ +{ + "id": "45c376e0-aaa4-477a-b865-814013536d18", + "prevId": "ad1a6acd-02d6-4107-9f5b-d13c98423842", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_users": { + "name": "app_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_encrypted": { + "name": "display_name_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "app_users_wallet_address_unique": { + "name": "app_users_wallet_address_unique", + "columns": [ + { + "expression": "lower(\"wallet_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_wallet_address": { + "name": "actor_wallet_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_table": { + "name": "subject_table", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_subject_idx": { + "name": "audit_events_subject_idx", + "columns": [ + { + "expression": "subject_table", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_actor_user_id_idx": { + "name": "audit_events_actor_user_id_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_quarter_id_idx": { + "name": "audit_events_quarter_id_idx", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_events_created_at_idx": { + "name": "audit_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_actor_user_id_app_users_id_fk": { + "name": "audit_events_actor_user_id_app_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "app_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_events_quarter_id_quarters_id_fk": { + "name": "audit_events_quarter_id_quarters_id_fk", + "tableFrom": "audit_events", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cleric_roles": { + "name": "cleric_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "wallet_address": { + "name": "wallet_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cleric_roles_wallet_address_idx": { + "name": "cleric_roles_wallet_address_idx", + "columns": [ + { + "expression": "lower(\"wallet_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cleric_roles_active_idx": { + "name": "cleric_roles_active_idx", + "columns": [ + { + "expression": "lower(\"wallet_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cleric_roles_granted_by_user_id_app_users_id_fk": { + "name": "cleric_roles_granted_by_user_id_app_users_id_fk", + "tableFrom": "cleric_roles", + "tableTo": "app_users", + "columnsFrom": [ + "granted_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cleric_roles_revoked_by_user_id_app_users_id_fk": { + "name": "cleric_roles_revoked_by_user_id_app_users_id_fk", + "tableFrom": "cleric_roles", + "tableTo": "app_users", + "columnsFrom": [ + "revoked_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dao_proposals": { + "name": "dao_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dao_address": { + "name": "dao_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposal_number": { + "name": "proposal_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_tx_hash": { + "name": "execution_tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "daohaus_url": { + "name": "daohaus_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_metadata": { + "name": "raw_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dao_proposals_chain_dao_proposal_unique": { + "name": "dao_proposals_chain_dao_proposal_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"dao_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dao_proposals_chain_execution_tx_unique": { + "name": "dao_proposals_chain_execution_tx_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"execution_tx_hash\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dao_proposals_executed_at_idx": { + "name": "dao_proposals_executed_at_idx", + "columns": [ + { + "expression": "executed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dao_proposals_status_idx": { + "name": "dao_proposals_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entities": { + "name": "entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name_encrypted": { + "name": "name_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "website_encrypted": { + "name": "website_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes_encrypted": { + "name": "notes_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_member": { + "name": "is_member", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entities_type_idx": { + "name": "entities_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entities_archived_at_idx": { + "name": "entities_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entity_addresses": { + "name": "entity_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label_encrypted": { + "name": "label_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entity_addresses_entity_id_idx": { + "name": "entity_addresses_entity_id_idx", + "columns": [ + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_addresses_address_idx": { + "name": "entity_addresses_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entity_addresses_chain_address_unique": { + "name": "entity_addresses_chain_address_unique", + "columns": [ + { + "expression": "coalesce(\"chain_id\", -1)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "lower(\"address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entity_addresses_entity_id_entities_id_fk": { + "name": "entity_addresses_entity_id_entities_id_fk", + "tableFrom": "entity_addresses", + "tableTo": "entities", + "columnsFrom": [ + "entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hat_steward_role_safes": { + "name": "hat_steward_role_safes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hat_id": { + "name": "hat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hat_pretty_id": { + "name": "hat_pretty_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hat_label": { + "name": "hat_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "safe_address": { + "name": "safe_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hat_steward_role_safes_hat_id_unique": { + "name": "hat_steward_role_safes_hat_id_unique", + "columns": [ + { + "expression": "hat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hat_steward_role_safes_safe_idx": { + "name": "hat_steward_role_safes_safe_idx", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"safe_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hat_steward_role_safes_archived_at_idx": { + "name": "hat_steward_role_safes_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_entries": { + "name": "ledger_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "ledger_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_external_id": { + "name": "source_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "ledger_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uncategorized'" + }, + "verification_status": { + "name": "verification_status", + "type": "verification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "treasury_account_id": { + "name": "treasury_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "treasury_transaction_transfer_id": { + "name": "treasury_transaction_transfer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "asset_symbol": { + "name": "asset_symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_amount": { + "name": "asset_amount", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": true + }, + "usd_amount": { + "name": "usd_amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "counterparty_entity_id": { + "name": "counterparty_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "raid_id": { + "name": "raid_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rip_id": { + "name": "rip_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes_encrypted": { + "name": "notes_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_entries_quarter_id_idx": { + "name": "ledger_entries_quarter_id_idx", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_category_idx": { + "name": "ledger_entries_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_source_external_id_unique": { + "name": "ledger_entries_source_external_id_unique", + "columns": [ + { + "expression": "source_external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_tx_hash_idx": { + "name": "ledger_entries_tx_hash_idx", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_treasury_transfer_unique": { + "name": "ledger_entries_treasury_transfer_unique", + "columns": [ + { + "expression": "treasury_transaction_transfer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_raid_id_idx": { + "name": "ledger_entries_raid_id_idx", + "columns": [ + { + "expression": "raid_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_rip_id_idx": { + "name": "ledger_entries_rip_id_idx", + "columns": [ + { + "expression": "rip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_occurred_at_idx": { + "name": "ledger_entries_occurred_at_idx", + "columns": [ + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_entries_quarter_id_quarters_id_fk": { + "name": "ledger_entries_quarter_id_quarters_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_treasury_account_id_treasury_accounts_id_fk": { + "name": "ledger_entries_treasury_account_id_treasury_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "treasury_accounts", + "columnsFrom": [ + "treasury_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_treasury_transaction_transfer_id_treasury_transaction_transfers_id_fk": { + "name": "ledger_entries_treasury_transaction_transfer_id_treasury_transaction_transfers_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "treasury_transaction_transfers", + "columnsFrom": [ + "treasury_transaction_transfer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_counterparty_entity_id_entities_id_fk": { + "name": "ledger_entries_counterparty_entity_id_entities_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "entities", + "columnsFrom": [ + "counterparty_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_raid_id_raids_id_fk": { + "name": "ledger_entries_raid_id_raids_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "raids", + "columnsFrom": [ + "raid_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_rip_id_rips_id_fk": { + "name": "ledger_entries_rip_id_rips_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "rips", + "columnsFrom": [ + "rip_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.machine_api_rate_limits": { + "name": "machine_api_rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "machine_api_rate_limits_reset_at_idx": { + "name": "machine_api_rate_limits_reset_at_idx", + "columns": [ + { + "expression": "reset_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.machine_api_request_nonces": { + "name": "machine_api_request_nonces", + "schema": "", + "columns": { + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegator_address": { + "name": "delegator_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "report_slice": { + "name": "report_slice", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "machine_api_request_nonces_agent_idx": { + "name": "machine_api_request_nonces_agent_idx", + "columns": [ + { + "expression": "lower(\"agent_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "machine_api_request_nonces_delegator_idx": { + "name": "machine_api_request_nonces_delegator_idx", + "columns": [ + { + "expression": "lower(\"delegator_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "machine_api_request_nonces_expires_at_idx": { + "name": "machine_api_request_nonces_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "machine_api_request_nonces_quarter_id_quarters_id_fk": { + "name": "machine_api_request_nonces_quarter_id_quarters_id_fk", + "tableFrom": "machine_api_request_nonces", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.membership_activities": { + "name": "membership_activities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dao_proposal_id": { + "name": "dao_proposal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "membership_activity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dao_address": { + "name": "dao_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "member_address": { + "name": "member_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_address": { + "name": "recipient_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposal_title": { + "name": "proposal_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_address": { + "name": "asset_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_symbol": { + "name": "asset_symbol", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_amount": { + "name": "asset_amount", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": false + }, + "usd_amount": { + "name": "usd_amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "shares": { + "name": "shares", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": false + }, + "loot": { + "name": "loot", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": false + }, + "raw_metadata": { + "name": "raw_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "membership_activities_chain_tx_type_member_unique": { + "name": "membership_activities_chain_tx_type_member_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tx_hash\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"member_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "membership_activities_quarter_id_idx": { + "name": "membership_activities_quarter_id_idx", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "membership_activities_dao_proposal_id_idx": { + "name": "membership_activities_dao_proposal_id_idx", + "columns": [ + { + "expression": "dao_proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "membership_activities_executed_at_idx": { + "name": "membership_activities_executed_at_idx", + "columns": [ + { + "expression": "executed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "membership_activities_type_idx": { + "name": "membership_activities_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "membership_activities_member_address_idx": { + "name": "membership_activities_member_address_idx", + "columns": [ + { + "expression": "member_address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "membership_activities_quarter_id_quarters_id_fk": { + "name": "membership_activities_quarter_id_quarters_id_fk", + "tableFrom": "membership_activities", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "membership_activities_dao_proposal_id_dao_proposals_id_fk": { + "name": "membership_activities_dao_proposal_id_dao_proposals_id_fk", + "tableFrom": "membership_activities", + "tableTo": "dao_proposals", + "columnsFrom": [ + "dao_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quarter_balance_snapshots": { + "name": "quarter_balance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "treasury_account_id": { + "name": "treasury_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "boundary": { + "name": "boundary", + "type": "quarter_balance_boundary", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_number": { + "name": "block_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_timestamp": { + "name": "block_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decimals": { + "name": "decimals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_amount": { + "name": "raw_amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": true + }, + "usd_price": { + "name": "usd_price", + "type": "numeric(18, 8)", + "primaryKey": false, + "notNull": true + }, + "usd_value": { + "name": "usd_value", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "price_source": { + "name": "price_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quarter_balance_snapshots_quarter_boundary_idx": { + "name": "quarter_balance_snapshots_quarter_boundary_idx", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quarter_balance_snapshots_unique": { + "name": "quarter_balance_snapshots_unique", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"account_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "symbol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quarter_balance_snapshots_quarter_id_quarters_id_fk": { + "name": "quarter_balance_snapshots_quarter_id_quarters_id_fk", + "tableFrom": "quarter_balance_snapshots", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "quarter_balance_snapshots_treasury_account_id_treasury_accounts_id_fk": { + "name": "quarter_balance_snapshots_treasury_account_id_treasury_accounts_id_fk", + "tableFrom": "quarter_balance_snapshots", + "tableTo": "treasury_accounts", + "columnsFrom": [ + "treasury_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quarter_balance_validations": { + "name": "quarter_balance_validations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "quarter_balance_validation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "checked_count": { + "name": "checked_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "variance_count": { + "name": "variance_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "excluded_count": { + "name": "excluded_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_variance_usd": { + "name": "total_variance_usd", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "acknowledgement_note_encrypted": { + "name": "acknowledgement_note_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "acknowledged_by_wallet_address": { + "name": "acknowledged_by_wallet_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_sync_run_id": { + "name": "source_sync_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quarter_balance_validations_quarter_id_unique": { + "name": "quarter_balance_validations_quarter_id_unique", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quarter_balance_validations_status_idx": { + "name": "quarter_balance_validations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quarter_balance_validations_quarter_id_quarters_id_fk": { + "name": "quarter_balance_validations_quarter_id_quarters_id_fk", + "tableFrom": "quarter_balance_validations", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quarter_sync_statuses": { + "name": "quarter_sync_statuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "quarter_id": { + "name": "quarter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_status": { + "name": "overall_status", + "type": "quarter_sync_overall_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "current_step": { + "name": "current_step", + "type": "quarter_sync_step", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "transactions_status": { + "name": "transactions_status", + "type": "quarter_sync_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "proposals_status": { + "name": "proposals_status", + "type": "quarter_sync_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "membership_status": { + "name": "membership_status", + "type": "quarter_sync_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "balances_status": { + "name": "balances_status", + "type": "quarter_sync_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "finalize_status": { + "name": "finalize_status", + "type": "quarter_sync_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "transactions_started_at": { + "name": "transactions_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "transactions_completed_at": { + "name": "transactions_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "proposals_started_at": { + "name": "proposals_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "proposals_completed_at": { + "name": "proposals_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "membership_started_at": { + "name": "membership_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "membership_completed_at": { + "name": "membership_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "balances_started_at": { + "name": "balances_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "balances_completed_at": { + "name": "balances_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finalize_started_at": { + "name": "finalize_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finalize_completed_at": { + "name": "finalize_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "transactions_error": { + "name": "transactions_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposals_error": { + "name": "proposals_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_error": { + "name": "membership_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "balances_error": { + "name": "balances_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalize_error": { + "name": "finalize_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "imported_transactions": { + "name": "imported_transactions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "imported_transfers": { + "name": "imported_transfers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scanned_transfers": { + "name": "scanned_transfers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_error_count": { + "name": "sync_error_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "proposal_linked_transactions": { + "name": "proposal_linked_transactions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "proposal_matches": { + "name": "proposal_matches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "membership_activities": { + "name": "membership_activities", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quarter_sync_statuses_quarter_id_unique": { + "name": "quarter_sync_statuses_quarter_id_unique", + "columns": [ + { + "expression": "quarter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quarter_sync_statuses_overall_status_idx": { + "name": "quarter_sync_statuses_overall_status_idx", + "columns": [ + { + "expression": "overall_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quarter_sync_statuses_last_synced_at_idx": { + "name": "quarter_sync_statuses_last_synced_at_idx", + "columns": [ + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "quarter_sync_statuses_quarter_id_quarters_id_fk": { + "name": "quarter_sync_statuses_quarter_id_quarters_id_fk", + "tableFrom": "quarter_sync_statuses", + "tableTo": "quarters", + "columnsFrom": [ + "quarter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quarters": { + "name": "quarters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quarter": { + "name": "quarter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_on": { + "name": "starts_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "ends_on": { + "name": "ends_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "quarter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reopened_at": { + "name": "reopened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "quarters_year_quarter_unique": { + "name": "quarters_year_quarter_unique", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "quarter", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "quarters_status_idx": { + "name": "quarters_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.raids": { + "name": "raids", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_entity_id": { + "name": "client_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name_encrypted": { + "name": "name_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "notes_encrypted": { + "name": "notes_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "raids_client_entity_id_idx": { + "name": "raids_client_entity_id_idx", + "columns": [ + { + "expression": "client_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "raids_archived_at_idx": { + "name": "raids_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "raids_client_entity_id_entities_id_fk": { + "name": "raids_client_entity_id_entities_id_fk", + "tableFrom": "raids", + "tableTo": "entities", + "columnsFrom": [ + "client_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report_assistant_rate_limits": { + "name": "report_assistant_rate_limits", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_assistant_rate_limits_reset_at_idx": { + "name": "report_assistant_rate_limits_reset_at_idx", + "columns": [ + { + "expression": "reset_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rips": { + "name": "rips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title_encrypted": { + "name": "title_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "url_encrypted": { + "name": "url_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_wallet_address": { + "name": "created_by_wallet_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rips_created_at_idx": { + "name": "rips_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rips_created_by_wallet_address_idx": { + "name": "rips_created_by_wallet_address_idx", + "columns": [ + { + "expression": "lower(\"created_by_wallet_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_accounts": { + "name": "treasury_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name_encrypted": { + "name": "name_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "treasury_account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "is_dao_controlled": { + "name": "is_dao_controlled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes_encrypted": { + "name": "notes_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "treasury_accounts_chain_address_unique": { + "name": "treasury_accounts_chain_address_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_accounts_type_idx": { + "name": "treasury_accounts_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_accounts_archived_at_idx": { + "name": "treasury_accounts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_balance_assets": { + "name": "treasury_balance_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decimals": { + "name": "decimals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_amount": { + "name": "raw_amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": true + }, + "usd_price": { + "name": "usd_price", + "type": "numeric(18, 8)", + "primaryKey": false, + "notNull": true + }, + "usd_value": { + "name": "usd_value", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "treasury_balance_assets_snapshot_id_idx": { + "name": "treasury_balance_assets_snapshot_id_idx", + "columns": [ + { + "expression": "snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_balance_assets_snapshot_symbol_unique": { + "name": "treasury_balance_assets_snapshot_symbol_unique", + "columns": [ + { + "expression": "snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "symbol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "treasury_balance_assets_snapshot_id_treasury_balance_snapshots_id_fk": { + "name": "treasury_balance_assets_snapshot_id_treasury_balance_snapshots_id_fk", + "tableFrom": "treasury_balance_assets", + "tableTo": "treasury_balance_snapshots", + "columnsFrom": [ + "snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_balance_snapshots": { + "name": "treasury_balance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "treasury_snapshot_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "total_usd": { + "name": "total_usd", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "treasury_balance_snapshots_chain_account_synced_idx": { + "name": "treasury_balance_snapshots_chain_account_synced_idx", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "synced_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_balance_snapshots_synced_at_idx": { + "name": "treasury_balance_snapshots_synced_at_idx", + "columns": [ + { + "expression": "synced_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_transaction_transfers": { + "name": "treasury_transaction_transfers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "treasury_transaction_id": { + "name": "treasury_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "treasury_account_id": { + "name": "treasury_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "transfer_id": { + "name": "transfer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "treasury_transfer_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "transfer_type": { + "name": "transfer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_address": { + "name": "to_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_address": { + "name": "token_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_symbol": { + "name": "asset_symbol", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_name": { + "name": "asset_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decimals": { + "name": "decimals", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_amount": { + "name": "raw_amount", + "type": "numeric(78, 0)", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(36, 18)", + "primaryKey": false, + "notNull": true + }, + "usd_price": { + "name": "usd_price", + "type": "numeric(18, 8)", + "primaryKey": false, + "notNull": false + }, + "usd_amount": { + "name": "usd_amount", + "type": "numeric(18, 2)", + "primaryKey": false, + "notNull": false + }, + "raw_metadata": { + "name": "raw_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "treasury_transaction_transfers_chain_transfer_unique": { + "name": "treasury_transaction_transfers_chain_transfer_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"account_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "transfer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transaction_transfers_transaction_id_idx": { + "name": "treasury_transaction_transfers_transaction_id_idx", + "columns": [ + { + "expression": "treasury_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transaction_transfers_treasury_account_id_idx": { + "name": "treasury_transaction_transfers_treasury_account_id_idx", + "columns": [ + { + "expression": "treasury_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transaction_transfers_tx_hash_idx": { + "name": "treasury_transaction_transfers_tx_hash_idx", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transaction_transfers_executed_at_idx": { + "name": "treasury_transaction_transfers_executed_at_idx", + "columns": [ + { + "expression": "executed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "treasury_transaction_transfers_treasury_transaction_id_treasury_transactions_id_fk": { + "name": "treasury_transaction_transfers_treasury_transaction_id_treasury_transactions_id_fk", + "tableFrom": "treasury_transaction_transfers", + "tableTo": "treasury_transactions", + "columnsFrom": [ + "treasury_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "treasury_transaction_transfers_treasury_account_id_treasury_accounts_id_fk": { + "name": "treasury_transaction_transfers_treasury_account_id_treasury_accounts_id_fk", + "tableFrom": "treasury_transaction_transfers", + "tableTo": "treasury_accounts", + "columnsFrom": [ + "treasury_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.treasury_transactions": { + "name": "treasury_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dao_proposal_id": { + "name": "dao_proposal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "treasury_account_id": { + "name": "treasury_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "ledger_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chain_id": { + "name": "chain_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tx_hash": { + "name": "tx_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "safe_transaction_hash": { + "name": "safe_transaction_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "block_number": { + "name": "block_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "raw_metadata": { + "name": "raw_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "imported_at": { + "name": "imported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "treasury_transactions_chain_account_tx_unique": { + "name": "treasury_transactions_chain_account_tx_unique", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"account_address\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "lower(\"tx_hash\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transactions_tx_hash_idx": { + "name": "treasury_transactions_tx_hash_idx", + "columns": [ + { + "expression": "tx_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transactions_account_executed_idx": { + "name": "treasury_transactions_account_executed_idx", + "columns": [ + { + "expression": "chain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "executed_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transactions_treasury_account_id_idx": { + "name": "treasury_transactions_treasury_account_id_idx", + "columns": [ + { + "expression": "treasury_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "treasury_transactions_dao_proposal_id_idx": { + "name": "treasury_transactions_dao_proposal_id_idx", + "columns": [ + { + "expression": "dao_proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "treasury_transactions_dao_proposal_id_dao_proposals_id_fk": { + "name": "treasury_transactions_dao_proposal_id_dao_proposals_id_fk", + "tableFrom": "treasury_transactions", + "tableTo": "dao_proposals", + "columnsFrom": [ + "dao_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "treasury_transactions_treasury_account_id_treasury_accounts_id_fk": { + "name": "treasury_transactions_treasury_account_id_treasury_accounts_id_fk", + "tableFrom": "treasury_transactions", + "tableTo": "treasury_accounts", + "columnsFrom": [ + "treasury_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "create", + "update", + "delete", + "import", + "classify", + "publish", + "reopen", + "grant_role", + "revoke_role" + ] + }, + "public.entity_type": { + "name": "entity_type", + "schema": "public", + "values": [ + "client", + "provider", + "subcontractor" + ] + }, + "public.ledger_category": { + "name": "ledger_category", + "schema": "public", + "values": [ + "raid_revenue", + "raid_spoils", + "subcontractor_payout", + "rip_expense", + "provider_expense", + "member_dues", + "ragequit", + "treasury_transfer", + "uncategorized" + ] + }, + "public.ledger_source": { + "name": "ledger_source", + "schema": "public", + "values": [ + "main_safe", + "side_vault", + "operator", + "manual", + "bank_csv", + "dao_proposal" + ] + }, + "public.membership_activity_type": { + "name": "membership_activity_type", + "schema": "public", + "values": [ + "join", + "ragequit" + ] + }, + "public.quarter_balance_boundary": { + "name": "quarter_balance_boundary", + "schema": "public", + "values": [ + "opening", + "closing" + ] + }, + "public.quarter_balance_validation_status": { + "name": "quarter_balance_validation_status", + "schema": "public", + "values": [ + "not_ready", + "needs_review", + "validated", + "acknowledged" + ] + }, + "public.quarter_status": { + "name": "quarter_status", + "schema": "public", + "values": [ + "draft", + "ready_for_review", + "published", + "reopened" + ] + }, + "public.quarter_sync_overall_status": { + "name": "quarter_sync_overall_status", + "schema": "public", + "values": [ + "idle", + "running", + "success", + "partial", + "failed" + ] + }, + "public.quarter_sync_step": { + "name": "quarter_sync_step", + "schema": "public", + "values": [ + "transactions", + "proposals", + "membership", + "balances", + "finalize" + ] + }, + "public.quarter_sync_step_status": { + "name": "quarter_sync_step_status", + "schema": "public", + "values": [ + "pending", + "running", + "success", + "failed" + ] + }, + "public.treasury_account_type": { + "name": "treasury_account_type", + "schema": "public", + "values": [ + "main_safe", + "side_vault", + "operator", + "bank" + ] + }, + "public.treasury_snapshot_status": { + "name": "treasury_snapshot_status", + "schema": "public", + "values": [ + "pending_live_sync", + "synced", + "stale_syncing", + "partial", + "failed" + ] + }, + "public.treasury_transfer_direction": { + "name": "treasury_transfer_direction", + "schema": "public", + "values": [ + "inflow", + "outflow", + "internal" + ] + }, + "public.verification_status": { + "name": "verification_status", + "schema": "public", + "values": [ + "verified", + "unverified" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1b228f7..cb11bfd 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1782756454663, "tag": "0019_magenta_deathbird", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1783301301037, + "tag": "0020_medical_mysterio", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/hat-stewards/actions.ts b/src/app/hat-stewards/actions.ts new file mode 100644 index 0000000..6ffc285 --- /dev/null +++ b/src/app/hat-stewards/actions.ts @@ -0,0 +1,240 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import { eq } from "drizzle-orm"; +import { gnosis } from "viem/chains"; + +import { getDb } from "@/db"; +import { hatStewardRoleSafes } from "@/db/schema"; +import { canUseAdminAccess, getAuthSession } from "@/lib/auth/session"; +import { writeAuditEvent } from "@/lib/audit"; +import { listHatOptions } from "@/lib/hat-stewards/hats"; +import { + getHatStewardRoleSafe, + normalizeRoleSafeAddress, +} from "@/lib/hat-stewards/role-safes"; + +const HAT_STEWARDS_PATH = "/hat-stewards"; + +export type HatStewardRoleSafeFormState = { + field?: "safeAddress"; + message: string; +}; + +function getString(formData: FormData, key: string) { + const value = formData.get(key); + + return typeof value === "string" ? value.trim() : ""; +} + +async function requireAdminSession() { + const session = await getAuthSession(); + + if (!canUseAdminAccess(session)) { + throw new Error("Admin access required"); + } + + return session; +} + +async function getSelectedHat(hatId: string) { + const hats = await listHatOptions(); + + if (hats.status !== "ready") { + throw new Error("Hats list is unavailable"); + } + + const hat = hats.hats.find((option) => option.prettyId === hatId); + + if (!hat) { + throw new Error("Selected hat was not found in the RaidGuild tree"); + } + + return hat; +} + +async function getHatFromForm(formData: FormData) { + const hatId = getString(formData, "hatId"); + + if (hatId) { + const hat = await getSelectedHat(hatId); + + return { + id: hat.prettyId, + label: hat.label, + prettyId: hat.prettyId, + }; + } + + const manualHatPrettyId = getString(formData, "manualHatPrettyId"); + const manualHatLabel = getString(formData, "manualHatLabel"); + + if (!manualHatPrettyId || !manualHatLabel) { + throw new Error("Hat is required"); + } + + if (!/^\d+(?:\.\d+)*$/.test(manualHatPrettyId)) { + throw new Error("Manual Hat ID must use dotted decimal format"); + } + + return { + id: manualHatPrettyId, + label: manualHatLabel, + prettyId: manualHatPrettyId, + }; +} + +export async function saveHatStewardRoleSafe(formData: FormData) { + const session = await requireAdminSession(); + const id = getString(formData, "id"); + const notes = getString(formData, "notes"); + const safeAddress = normalizeRoleSafeAddress(getString(formData, "safeAddress")); + const hat = await getHatFromForm(formData); + const db = getDb(); + + if (id) { + await db + .update(hatStewardRoleSafes) + .set({ + chainId: gnosis.id, + hatId: hat.id, + hatLabel: hat.label, + hatPrettyId: hat.prettyId, + notes: notes || null, + safeAddress, + }) + .where(eq(hatStewardRoleSafes.id, id)); + + await writeAuditEvent({ + action: "update", + actorWalletAddress: session.address, + metadata: { hatPrettyId: hat.prettyId }, + subjectId: id, + subjectTable: "hat_steward_role_safes", + summary: "Updated Hat Steward role Safe", + }); + } else { + const [roleSafe] = await db + .insert(hatStewardRoleSafes) + .values({ + chainId: gnosis.id, + hatId: hat.id, + hatLabel: hat.label, + hatPrettyId: hat.prettyId, + notes: notes || null, + safeAddress, + }) + .onConflictDoUpdate({ + set: { + archivedAt: null, + chainId: gnosis.id, + hatLabel: hat.label, + hatPrettyId: hat.prettyId, + notes: notes || null, + safeAddress, + }, + target: hatStewardRoleSafes.hatId, + }) + .returning(); + + await writeAuditEvent({ + action: "update", + actorWalletAddress: session.address, + metadata: { hatPrettyId: hat.prettyId }, + subjectId: roleSafe.id, + subjectTable: "hat_steward_role_safes", + summary: "Saved Hat Steward role Safe", + }); + } + + revalidatePath(HAT_STEWARDS_PATH); + redirect(HAT_STEWARDS_PATH); +} + +export async function saveHatStewardRoleSafeWithState( + _previousState: HatStewardRoleSafeFormState, + formData: FormData, +): Promise { + try { + await saveHatStewardRoleSafe(formData); + } catch (error) { + if ( + error instanceof Error && + error.message === "Safe address must be a valid EVM address" + ) { + return { + field: "safeAddress", + message: "Enter a valid EVM Safe address.", + }; + } + + throw error; + } + + return { message: "" }; +} + +export async function archiveHatStewardRoleSafe(formData: FormData) { + const session = await requireAdminSession(); + const id = getString(formData, "id"); + + if (!id) { + throw new Error("Hat Steward mapping is required"); + } + + const roleSafe = await getHatStewardRoleSafe(id); + + if (!roleSafe) { + throw new Error("Hat Steward mapping not found"); + } + + await getDb() + .update(hatStewardRoleSafes) + .set({ archivedAt: new Date() }) + .where(eq(hatStewardRoleSafes.id, id)); + + await writeAuditEvent({ + action: "update", + actorWalletAddress: session.address, + metadata: { hatPrettyId: roleSafe.hatPrettyId }, + subjectId: id, + subjectTable: "hat_steward_role_safes", + summary: "Archived Hat Steward role Safe", + }); + + revalidatePath(HAT_STEWARDS_PATH); + redirect(HAT_STEWARDS_PATH); +} + +export async function restoreHatStewardRoleSafe(formData: FormData) { + const session = await requireAdminSession(); + const id = getString(formData, "id"); + + if (!id) { + throw new Error("Hat Steward mapping is required"); + } + + const roleSafe = await getHatStewardRoleSafe(id); + + if (!roleSafe) { + throw new Error("Hat Steward mapping not found"); + } + + await getDb() + .update(hatStewardRoleSafes) + .set({ archivedAt: null }) + .where(eq(hatStewardRoleSafes.id, id)); + + await writeAuditEvent({ + action: "update", + actorWalletAddress: session.address, + metadata: { hatPrettyId: roleSafe.hatPrettyId }, + subjectId: id, + subjectTable: "hat_steward_role_safes", + summary: "Restored Hat Steward role Safe", + }); + + revalidatePath(HAT_STEWARDS_PATH); + redirect(HAT_STEWARDS_PATH); +} diff --git a/src/app/hat-stewards/hat-steward-admin-form.tsx b/src/app/hat-stewards/hat-steward-admin-form.tsx new file mode 100644 index 0000000..9459e52 --- /dev/null +++ b/src/app/hat-stewards/hat-steward-admin-form.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { Archive, Plus, RotateCcw, Save, ShieldCheck } from "lucide-react"; +import Link from "next/link"; +import { type ReactNode, useActionState, useMemo, useState } from "react"; + +import { + archiveHatStewardRoleSafe, + restoreHatStewardRoleSafe, + saveHatStewardRoleSafeWithState, + type HatStewardRoleSafeFormState, +} from "@/app/hat-stewards/actions"; +import type { HatOption, HatOptionsResult } from "@/lib/hat-stewards/hats"; +import type { HatStewardRoleSafe } from "@/lib/hat-stewards/role-safes"; + +const EMPTY_FORM_STATE: HatStewardRoleSafeFormState = { message: "" }; + +function getHatOptionLabel(hat: HatOption) { + return `${hat.prettyId} - ${hat.label}`; +} + +function getModalHref(modal: "add-mapping") { + return `/hat-stewards?modal=${modal}`; +} + +function ModalShell({ + children, + title, +}: { + children: ReactNode; + title: string; +}) { + return ( +
+ +
+
+
+
+
+
+

Hat Steward

+

{title}

+
+
+ + Close + +
+ {children} +
+
+ ); +} + +function ManualHatFields() { + return ( +
+ + +
+ ); +} + +export function HatStewardAdminForm({ + hatOptions, + isAddModalOpen, + roleSafes, +}: { + hatOptions: HatOptionsResult; + isAddModalOpen: boolean; + roleSafes: HatStewardRoleSafe[]; +}) { + const [query, setQuery] = useState(""); + const [formState, formAction] = useActionState( + saveHatStewardRoleSafeWithState, + EMPTY_FORM_STATE, + ); + const activeMappings = new Set( + roleSafes.flatMap((roleSafe) => [roleSafe.hatId, roleSafe.hatPrettyId]), + ); + const hats = hatOptions.hats; + const hatsReady = hatOptions.status === "ready"; + const filteredHats = useMemo(() => { + const needle = query.trim().toLowerCase(); + + if (!needle) { + return hats; + } + + return hats.filter((hat) => + `${hat.prettyId} ${hat.label}`.toLowerCase().includes(needle), + ); + }, [hats, query]); + + return ( +
+
+
+

Admin

+

Role Safe mappings

+
+ +
+ + {hatOptions.status === "unavailable" ? ( +
+ {hatOptions.message} +
+ ) : null} + + {roleSafes.length > 0 ? ( +
+ {roleSafes.map((roleSafe) => { + const archived = Boolean(roleSafe.archivedAt); + + return ( +
+
+

{roleSafe.hatLabel}

+

+ {roleSafe.hatPrettyId} - {roleSafe.safeAddress} +

+ {roleSafe.notes ? ( +

+ {roleSafe.notes} +

+ ) : null} +
+
+ + +
+
+ ); + })} +
+ ) : null} + + {isAddModalOpen ? ( + +
+ {hatsReady ? ( +
+ + setQuery(event.target.value)} + className="h-11 rounded-md border border-input bg-background px-3 text-sm outline-none transition-all focus-visible:ring-2 focus-visible:ring-ring" + placeholder="Search hats" + /> + +
+ ) : ( + + )} + +
+ + +
+ + + +
+ ) : null} +
+ ); +} diff --git a/src/app/hat-stewards/page.tsx b/src/app/hat-stewards/page.tsx new file mode 100644 index 0000000..d1037a6 --- /dev/null +++ b/src/app/hat-stewards/page.tsx @@ -0,0 +1,358 @@ +import { + BadgeDollarSign, + ExternalLink, + ShieldCheck, + Wallet, + type LucideIcon, +} from "lucide-react"; + +import { HatStewardAdminForm } from "@/app/hat-stewards/hat-steward-admin-form"; +import { AppHeader } from "@/components/app-header"; +import { DashboardBackLink } from "@/components/dashboard-back-link"; +import { + canUseAdminAccess, + getAuthSession, + serializeSession, +} from "@/lib/auth/session"; +import { + listHatOptions, + type HatOptionsResult, +} from "@/lib/hat-stewards/hats"; +import { + listHatStewardRoleSafes, + type HatStewardRoleSafe, +} from "@/lib/hat-stewards/role-safes"; +import { + listIncomingStreamsByReceiver, + getStreamRunwayForStreams, + type HatStewardStream, +} from "@/lib/hat-stewards/streams"; + +type SearchParams = Promise<{ + modal?: string | string[]; +}>; + +type SessionState = ReturnType; + +function getQueryValue(value: string | string[] | undefined) { + return Array.isArray(value) ? value[0] : value; +} + +function formatAddress(address: string) { + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +function formatTokenAmount(value: number) { + return new Intl.NumberFormat("en-US", { + maximumFractionDigits: value >= 100 ? 0 : 2, + minimumFractionDigits: 0, + }).format(value); +} + +function formatRunwayDate(value: string | null) { + if (!value) { + return "No projected runout"; + } + + return `Runs out ${new Intl.DateTimeFormat("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }).format(new Date(value))}`; +} + +function getProjectedQuarterSummary(streams: HatStewardStream[]) { + const amountsBySymbol = new Map(); + + for (const stream of streams) { + amountsBySymbol.set( + stream.tokenSymbol, + (amountsBySymbol.get(stream.tokenSymbol) ?? 0) + + stream.projectedQuarterAmount, + ); + } + + const amounts = Array.from(amountsBySymbol.entries()).filter( + ([, amount]) => amount > 0, + ); + + if (amounts.length === 0) { + return { detail: undefined, value: "No stream" }; + } + + if (amounts.length === 1) { + const [symbol, amount] = amounts[0]; + + return { detail: undefined, value: `${formatTokenAmount(amount)} ${symbol}` }; + } + + return { + detail: amounts + .map(([symbol, amount]) => `${formatTokenAmount(amount)} ${symbol}`) + .join(", "), + value: "Mixed tokens", + }; +} + +function parseHatStewardModal(value: string | string[] | undefined) { + const modal = getQueryValue(value); + + return modal === "add-mapping" ? modal : null; +} + +function AccessRequired({ session }: { session: SessionState }) { + return ( +
+ +
+
+

Hat Stewards

+

+ Member access required +

+
+
+
+ ); +} + +function MetricCard({ + detail, + icon: Icon, + label, + value, +}: { + detail?: string; + icon: LucideIcon; + label: string; + value: string; +}) { + return ( +
+
+ + +
+

{label}

+

{value}

+ {detail ? ( +

{detail}

+ ) : null} +
+
+
+ ); +} + +function RoleStreamCard({ + roleSafe, + streams, +}: { + roleSafe: HatStewardRoleSafe; + streams: HatStewardStream[]; +}) { + const projectedQuarter = getProjectedQuarterSummary(streams); + + return ( +
+
+
+

+ {roleSafe.hatPrettyId} +

+

{roleSafe.hatLabel}

+
+
+

Per quarter

+

{projectedQuarter.value}

+ {projectedQuarter.detail ? ( +

+ {projectedQuarter.detail} +

+ ) : null} +
+
+ + + + {streams.length > 0 ? ( +
+ {streams.map((stream) => ( +
+
+

+ {stream.tokenSymbol} stream +

+

+ From {formatAddress(stream.sender)} +

+
+
+

+ {formatTokenAmount(stream.projectedQuarterAmount)}{" "} + {stream.tokenSymbol} +

+

+ {formatTokenAmount(stream.flowRatePerDay)} / day +

+
+
+ ))} +
+ ) : ( +
+ No active incoming Superfluid streams found. +
+ )} +
+ ); +} + +export default async function HatStewardsPage({ + searchParams, +}: { + searchParams: SearchParams; +}) { + const sessionData = await getAuthSession(); + const session = serializeSession(sessionData); + const params = await searchParams; + const modal = parseHatStewardModal(params.modal); + + if (!session.authenticated || !session.permissions?.canAccess) { + return ; + } + + const canAdmin = canUseAdminAccess(sessionData); + const emptyHatOptions: HatOptionsResult = { hats: [], status: "ready" }; + const [hatOptions, roleSafes] = await Promise.all([ + canAdmin ? listHatOptions() : Promise.resolve(emptyHatOptions), + listHatStewardRoleSafes(), + ]); + const activeRoleSafes = roleSafes.filter((roleSafe) => !roleSafe.archivedAt); + const streamsResult = await listIncomingStreamsByReceiver( + activeRoleSafes.map((roleSafe) => roleSafe.safeAddress), + ); + const allActiveStreams = Array.from( + streamsResult.streamsByReceiver.values(), + ).flat(); + const runwayResult = await getStreamRunwayForStreams(allActiveStreams); + const projectedQuarter = getProjectedQuarterSummary(allActiveStreams); + + return ( +
+ + +
+ + +
+
+

DAO

+

Hat Stewards

+

+ Paid operational roles, their Safes, and active Superfluid streams. +

+
+
+ + {canAdmin ? ( + + ) : null} + +
+ + + +
+ + {streamsResult.status === "unavailable" ? ( +
+ {streamsResult.message} +
+ ) : null} + {runwayResult.status === "unavailable" ? ( +
+ {runwayResult.message} +
+ ) : null} + +
+
+

Streams

+

Role funding

+
+ + {activeRoleSafes.length > 0 ? ( +
+ {activeRoleSafes.map((roleSafe) => ( + + ))} +
+ ) : ( +
+ No Hat Steward Safes have been mapped yet. +
+ )} +
+ +
+
+ ); +} diff --git a/src/components/app-header.tsx b/src/components/app-header.tsx index 271076d..4691e57 100644 --- a/src/components/app-header.tsx +++ b/src/components/app-header.tsx @@ -38,6 +38,7 @@ const accountingLinks: NavLink[] = [ const daoLinks: NavLink[] = [ { href: "/portfolio", label: "Portfolio" }, + { href: "/hat-stewards", label: "Hat Stewards" }, { href: "/proposals", label: "Proposals" }, { href: "/membership", label: "Membership" }, ]; diff --git a/src/db/schema.ts b/src/db/schema.ts index fdaaebf..85680bb 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -352,6 +352,29 @@ export const treasuryAccounts = pgTable( ], ); +export const hatStewardRoleSafes = pgTable( + "hat_steward_role_safes", + { + id: uuid("id").defaultRandom().primaryKey(), + hatId: text("hat_id").notNull(), + hatPrettyId: text("hat_pretty_id").notNull(), + hatLabel: text("hat_label").notNull(), + safeAddress: text("safe_address").notNull(), + chainId: integer("chain_id").default(100).notNull(), + notes: text("notes"), + archivedAt: timestamp("archived_at", { withTimezone: true }), + ...timestamps, + }, + (table) => [ + uniqueIndex("hat_steward_role_safes_hat_id_unique").on(table.hatId), + index("hat_steward_role_safes_safe_idx").on( + table.chainId, + sql`lower(${table.safeAddress})`, + ), + index("hat_steward_role_safes_archived_at_idx").on(table.archivedAt), + ], +); + export const treasuryBalanceSnapshots = pgTable( "treasury_balance_snapshots", { diff --git a/src/lib/hat-stewards/hats.ts b/src/lib/hat-stewards/hats.ts new file mode 100644 index 0000000..d955ab9 --- /dev/null +++ b/src/lib/hat-stewards/hats.ts @@ -0,0 +1,319 @@ +import "server-only"; + +const DEFAULT_TREE_ID = 92; +const DEFAULT_CHAIN_ID = 100; +const DEFAULT_HATS_APP_BASE_URL = "https://app.hatsprotocol.xyz"; +const DEFAULT_IPFS_GATEWAY_URL = "https://ipfs.io/ipfs"; +const METADATA_FETCH_CONCURRENCY = 12; + +type HatsGraphResponse = { + data?: { + tree?: { + hats: { + currentSupply: string; + details: string; + id: string; + prettyId: string; + status: boolean; + wearers: { id: string }[]; + }[]; + } | null; + }; + errors?: { message: string }[]; +}; + +type HatMetadataResponse = { + data?: { + description?: string; + name?: string; + }; + description?: string; + name?: string; +}; + +export type HatOption = { + currentSupply: number; + id: string; + label: string; + prettyId: string; + status: boolean; + url: string; + wearerCount: number; +}; + +export type HatOptionsResult = + | { hats: HatOption[]; status: "ready" } + | { hats: HatOption[]; message: string; status: "unavailable" }; + +function getHatsSubgraphUrl() { + return process.env.HATS_SUBGRAPH_URL ?? ""; +} + +function getHatsSubgraphHeaders() { + const headers: Record = { + "content-type": "application/json", + }; + const authToken = process.env.HATS_SUBGRAPH_AUTH_TOKEN?.trim(); + + if (authToken) { + headers.authorization = authToken.toLowerCase().startsWith("bearer ") + ? authToken + : `Bearer ${authToken}`; + } + + return headers; +} + +function getTreeId() { + const value = process.env.HAT_STEWARDS_TREE_ID; + + if (!value) { + return DEFAULT_TREE_ID; + } + + const treeId = Number(value); + + if (!Number.isInteger(treeId) || treeId <= 0) { + throw new Error("HAT_STEWARDS_TREE_ID must be a positive integer"); + } + + return treeId; +} + +function getChainId() { + const value = process.env.HAT_STEWARDS_CHAIN_ID; + + if (!value) { + return DEFAULT_CHAIN_ID; + } + + const chainId = Number(value); + + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error("HAT_STEWARDS_CHAIN_ID must be a positive integer"); + } + + return chainId; +} + +function getHatsAppBaseUrl() { + return process.env.HATS_APP_BASE_URL ?? DEFAULT_HATS_APP_BASE_URL; +} + +function getIpfsGatewayUrl() { + return process.env.HATS_IPFS_GATEWAY_URL ?? DEFAULT_IPFS_GATEWAY_URL; +} + +function getIpfsMetadataUrl(uri: string) { + if (uri.startsWith("ipfs://")) { + const cid = uri.slice("ipfs://".length); + + return `${getIpfsGatewayUrl().replace(/\/$/, "")}/${cid}`; + } + + if (uri.startsWith("http://") || uri.startsWith("https://")) { + return uri; + } + + return null; +} + +async function resolveHatMetadataName(details: string) { + const metadataUrl = getIpfsMetadataUrl(details.trim()); + + if (!metadataUrl) { + return null; + } + + try { + const response = await fetch(metadataUrl, { + next: { revalidate: 300 }, + }); + + if (!response.ok) { + return null; + } + + const body = (await response.json()) as HatMetadataResponse; + const name = body.data?.name ?? body.name; + + return name?.trim() || null; + } catch { + return null; + } +} + +async function mapWithConcurrency( + values: TValue[], + limit: number, + mapper: (value: TValue, index: number) => Promise, +) { + const results = new Array(values.length); + let nextIndex = 0; + + async function worker() { + for (;;) { + const index = nextIndex; + nextIndex += 1; + + if (index >= values.length) { + return; + } + + results[index] = await mapper(values[index], index); + } + } + + await Promise.all( + Array.from({ length: Math.min(limit, values.length) }, () => worker()), + ); + + return results; +} + +function getTreeEntityId(treeId: number) { + return `0x${treeId.toString(16).padStart(8, "0")}`; +} + +function getDecimalPrettyId(hatId: string) { + const raw = hatId.toLowerCase().replace(/^0x/, ""); + const chunks = [raw.slice(0, 8)]; + + for (let index = 8; index < raw.length; index += 4) { + chunks.push(raw.slice(index, index + 4)); + } + + return chunks + .map((chunk) => Number.parseInt(chunk || "0", 16)) + .filter((value, index) => index === 0 || value > 0) + .join("."); +} + +async function getHatLabel(details: string, prettyId: string) { + const cleanDetails = details.trim(); + + if (!cleanDetails) { + return `Hat ${prettyId}`; + } + + const metadataName = await resolveHatMetadataName(cleanDetails); + + if (metadataName) { + return metadataName; + } + + if (cleanDetails.startsWith("ipfs://")) { + return `Hat ${prettyId}`; + } + + return cleanDetails.length > 96 + ? `${cleanDetails.slice(0, 93).trim()}...` + : cleanDetails; +} + +function getHatUrl({ chainId, prettyId, treeId }: { + chainId: number; + prettyId: string; + treeId: number; +}) { + return `${getHatsAppBaseUrl()}/trees/${chainId}/${treeId}?hatId=${encodeURIComponent( + prettyId, + )}`; +} + +export function getHatStewardsChainId() { + return getChainId(); +} + +export function getHatStewardsTreeId() { + return getTreeId(); +} + +export async function listHatOptions(): Promise { + const endpoint = getHatsSubgraphUrl(); + + if (!endpoint) { + return { + hats: [], + message: "Set HATS_SUBGRAPH_URL to fetch RaidGuild Hats.", + status: "unavailable", + }; + } + + const treeId = getTreeId(); + const chainId = getChainId(); + const response = await fetch(endpoint, { + body: JSON.stringify({ + query: ` + query RaidGuildHatStewards($treeId: ID!) { + tree(id: $treeId) { + hats(first: 500, orderBy: id, orderDirection: asc) { + id + prettyId + status + details + currentSupply + wearers { + id + } + } + } + } + `, + variables: { treeId: getTreeEntityId(treeId) }, + }), + headers: getHatsSubgraphHeaders(), + next: { revalidate: 300 }, + method: "POST", + }); + + if (!response.ok) { + return { + hats: [], + message: `Hats subgraph request failed: ${response.status}`, + status: "unavailable", + }; + } + + const body = (await response.json()) as HatsGraphResponse; + + if (body.errors?.length) { + return { + hats: [], + message: body.errors.map((error) => error.message).join("; "), + status: "unavailable", + }; + } + + const treeHats = body.data?.tree?.hats ?? []; + const hats = await mapWithConcurrency( + treeHats, + METADATA_FETCH_CONCURRENCY, + async (hat) => { + const prettyId = getDecimalPrettyId(hat.id); + + return { + currentSupply: Number(hat.currentSupply), + id: hat.id, + label: await getHatLabel(hat.details, prettyId), + prettyId, + status: hat.status, + url: getHatUrl({ chainId, prettyId, treeId }), + wearerCount: hat.wearers.length, + }; + }, + ); + + return { + hats, + status: "ready", + }; +} + +export function getHatUrlFromPrettyId(prettyId: string) { + return getHatUrl({ + chainId: getChainId(), + prettyId, + treeId: getTreeId(), + }); +} diff --git a/src/lib/hat-stewards/role-safes.ts b/src/lib/hat-stewards/role-safes.ts new file mode 100644 index 0000000..1115d0d --- /dev/null +++ b/src/lib/hat-stewards/role-safes.ts @@ -0,0 +1,80 @@ +import "server-only"; + +import { asc, eq } from "drizzle-orm"; +import { getAddress, isAddress } from "viem"; +import { gnosis } from "viem/chains"; + +import { getDb } from "@/db"; +import { hatStewardRoleSafes } from "@/db/schema"; +import { getHatUrlFromPrettyId } from "@/lib/hat-stewards/hats"; + +export type HatStewardRoleSafe = { + archivedAt: string | null; + chainId: number; + createdAt: string; + hatId: string; + hatLabel: string; + hatPrettyId: string; + hatUrl: string; + id: string; + notes: string | null; + safeAddress: `0x${string}`; + safeUrl: string; + updatedAt: string; +}; + +export function normalizeRoleSafeAddress(address: string) { + if (!isAddress(address, { strict: false })) { + throw new Error("Safe address must be a valid EVM address"); + } + + return getAddress(address); +} + +export function getSafeUrl(address: `0x${string}`, chainId: number = gnosis.id) { + if (chainId === gnosis.id) { + return `https://app.safe.global/home?safe=gno:${address}`; + } + + return `https://app.safe.global/home?safe=${chainId}:${address}`; +} + +function mapRoleSafe( + row: typeof hatStewardRoleSafes.$inferSelect, +): HatStewardRoleSafe { + const safeAddress = normalizeRoleSafeAddress(row.safeAddress); + + return { + archivedAt: row.archivedAt?.toISOString() ?? null, + chainId: row.chainId, + createdAt: row.createdAt.toISOString(), + hatId: row.hatId, + hatLabel: row.hatLabel, + hatPrettyId: row.hatPrettyId, + hatUrl: getHatUrlFromPrettyId(row.hatPrettyId), + id: row.id, + notes: row.notes, + safeAddress, + safeUrl: getSafeUrl(safeAddress, row.chainId), + updatedAt: row.updatedAt.toISOString(), + }; +} + +export async function listHatStewardRoleSafes() { + const rows = await getDb() + .select() + .from(hatStewardRoleSafes) + .orderBy(asc(hatStewardRoleSafes.archivedAt), asc(hatStewardRoleSafes.hatPrettyId)); + + return rows.map(mapRoleSafe); +} + +export async function getHatStewardRoleSafe(id: string) { + const [row] = await getDb() + .select() + .from(hatStewardRoleSafes) + .where(eq(hatStewardRoleSafes.id, id)) + .limit(1); + + return row ? mapRoleSafe(row) : null; +} diff --git a/src/lib/hat-stewards/streams.ts b/src/lib/hat-stewards/streams.ts new file mode 100644 index 0000000..5da312a --- /dev/null +++ b/src/lib/hat-stewards/streams.ts @@ -0,0 +1,300 @@ +import "server-only"; + +import { formatUnits, getAddress, type Address } from "viem"; + +const DEFAULT_SUPERFLUID_ENDPOINT = + "https://subgraph-endpoints.superfluid.dev/xdai-mainnet/protocol-v1"; +const QUARTER_SECONDS = BigInt(Math.round((60 * 60 * 24 * 365.25) / 4)); + +type SuperfluidStreamResponse = { + data?: { + streams: { + currentFlowRate: string; + id: string; + receiver: { id: string }; + sender: { id: string }; + token: { + decimals: number; + id: string; + name: string; + symbol: string; + }; + updatedAtTimestamp: string; + }[]; + }; + errors?: { message: string }[]; +}; + +type SuperfluidSnapshotResponse = { + data?: { + accountTokenSnapshots: { + balanceUntilUpdatedAt: string; + id: string; + token: { + decimals: number; + id: string; + symbol: string; + }; + totalNetFlowRate: string; + updatedAtTimestamp: string; + }[]; + }; + errors?: { message: string }[]; +}; + +type SuperfluidStreamRow = NonNullable< + SuperfluidStreamResponse["data"] +>["streams"][number]; + +export type HatStewardStream = { + currentFlowRate: string; + flowRatePerDay: number; + id: string; + projectedQuarterAmount: number; + receiver: Address; + sender: Address; + tokenAddress: Address; + tokenDecimals: number; + tokenName: string; + tokenSymbol: string; + updatedAt: string; +}; + +export type HatStewardStreamsResult = + | { status: "ready"; streamsByReceiver: Map } + | { + message: string; + status: "unavailable"; + streamsByReceiver: Map; + }; + +export type HatStewardStreamRunway = { + balanceAmount: number; + depletionRatePerDay: number; + estimatedRunOutAt: string | null; + senderCount: number; + tokenSymbol: string; +}; + +export type HatStewardStreamRunwayResult = + | { runway: HatStewardStreamRunway | null; status: "ready" } + | { message: string; runway: null; status: "unavailable" }; + +function getSuperfluidEndpoint() { + return process.env.SUPERFLUID_SUBGRAPH_URL ?? DEFAULT_SUPERFLUID_ENDPOINT; +} + +function getTokenAmount(rawAmount: bigint, decimals: number) { + return Number(formatUnits(rawAmount, decimals)); +} + +function mapStream( + stream: SuperfluidStreamRow, +): HatStewardStream { + const flowRate = BigInt(stream.currentFlowRate); + const projectedQuarterRaw = flowRate * QUARTER_SECONDS; + const flowRatePerDayRaw = flowRate * BigInt(60 * 60 * 24); + + return { + currentFlowRate: stream.currentFlowRate, + flowRatePerDay: getTokenAmount(flowRatePerDayRaw, stream.token.decimals), + id: stream.id, + projectedQuarterAmount: getTokenAmount( + projectedQuarterRaw, + stream.token.decimals, + ), + receiver: getAddress(stream.receiver.id), + sender: getAddress(stream.sender.id), + tokenAddress: getAddress(stream.token.id), + tokenDecimals: stream.token.decimals, + tokenName: stream.token.name, + tokenSymbol: stream.token.symbol, + updatedAt: new Date(Number(stream.updatedAtTimestamp) * 1000).toISOString(), + }; +} + +export async function listIncomingStreamsByReceiver( + receivers: Address[], +): Promise { + const streamsByReceiver = new Map(); + + for (const receiver of receivers) { + streamsByReceiver.set(receiver.toLowerCase(), []); + } + + if (receivers.length === 0) { + return { status: "ready", streamsByReceiver }; + } + + const response = await fetch(getSuperfluidEndpoint(), { + body: JSON.stringify({ + query: ` + query HatStewardStreams($receivers: [String!]!) { + streams( + first: 100 + orderBy: updatedAtTimestamp + orderDirection: desc + where: { receiver_in: $receivers, currentFlowRate_gt: "0" } + ) { + id + currentFlowRate + updatedAtTimestamp + sender { + id + } + receiver { + id + } + token { + id + name + symbol + decimals + } + } + } + `, + variables: { + receivers: receivers.map((receiver) => receiver.toLowerCase()), + }, + }), + headers: { "content-type": "application/json" }, + next: { revalidate: 300 }, + method: "POST", + }); + + if (!response.ok) { + return { + message: `Superfluid subgraph request failed: ${response.status}`, + status: "unavailable", + streamsByReceiver, + }; + } + + const body = (await response.json()) as SuperfluidStreamResponse; + + if (body.errors?.length) { + return { + message: body.errors.map((error) => error.message).join("; "), + status: "unavailable", + streamsByReceiver, + }; + } + + for (const stream of body.data?.streams ?? []) { + const receiver = stream.receiver.id.toLowerCase(); + const streams = streamsByReceiver.get(receiver) ?? []; + + streams.push(mapStream(stream)); + streamsByReceiver.set(receiver, streams); + } + + return { status: "ready", streamsByReceiver }; +} + +export async function getStreamRunwayForStreams( + streams: HatStewardStream[], +): Promise { + const snapshotIds = Array.from( + new Set( + streams.map( + (stream) => + `${stream.sender.toLowerCase()}-${stream.tokenAddress.toLowerCase()}`, + ), + ), + ); + + if (snapshotIds.length === 0) { + return { runway: null, status: "ready" }; + } + + const response = await fetch(getSuperfluidEndpoint(), { + body: JSON.stringify({ + query: ` + query HatStewardRunway($ids: [String!]!) { + accountTokenSnapshots(first: 100, where: { id_in: $ids }) { + id + balanceUntilUpdatedAt + totalNetFlowRate + updatedAtTimestamp + token { + id + symbol + decimals + } + } + } + `, + variables: { ids: snapshotIds }, + }), + headers: { "content-type": "application/json" }, + next: { revalidate: 300 }, + method: "POST", + }); + + if (!response.ok) { + return { + message: `Superfluid runway request failed: ${response.status}`, + runway: null, + status: "unavailable", + }; + } + + const body = (await response.json()) as SuperfluidSnapshotResponse; + + if (body.errors?.length) { + return { + message: body.errors.map((error) => error.message).join("; "), + runway: null, + status: "unavailable", + }; + } + + const snapshots = body.data?.accountTokenSnapshots ?? []; + const tokenSymbols = new Set(snapshots.map((snapshot) => snapshot.token.symbol)); + + if (snapshots.length === 0 || tokenSymbols.size !== 1) { + return { runway: null, status: "ready" }; + } + + const nowSeconds = BigInt(Math.floor(Date.now() / 1000)); + const decimals = snapshots[0]?.token.decimals ?? 18; + const tokenSymbol = snapshots[0]?.token.symbol ?? ""; + let balanceRaw = BigInt(0); + let depletionRateRaw = BigInt(0); + + for (const snapshot of snapshots) { + const updatedAt = BigInt(snapshot.updatedAtTimestamp); + const elapsed = + nowSeconds > updatedAt ? nowSeconds - updatedAt : BigInt(0); + const netFlowRate = BigInt(snapshot.totalNetFlowRate); + const currentBalance = BigInt(snapshot.balanceUntilUpdatedAt) + netFlowRate * elapsed; + + balanceRaw += currentBalance > BigInt(0) ? currentBalance : BigInt(0); + + if (netFlowRate < BigInt(0)) { + depletionRateRaw += -netFlowRate; + } + } + + const estimatedRunOutAt = + depletionRateRaw > BigInt(0) && balanceRaw > BigInt(0) + ? new Date( + (Number(nowSeconds) + Number(balanceRaw / depletionRateRaw)) * 1000, + ).toISOString() + : null; + + return { + runway: { + balanceAmount: getTokenAmount(balanceRaw, decimals), + depletionRatePerDay: getTokenAmount( + depletionRateRaw * BigInt(60 * 60 * 24), + decimals, + ), + estimatedRunOutAt, + senderCount: snapshots.length, + tokenSymbol, + }, + status: "ready", + }; +} diff --git a/tests/e2e/responsive-dashboard.spec.ts b/tests/e2e/responsive-dashboard.spec.ts index 8fe565b..1b1bf6c 100644 --- a/tests/e2e/responsive-dashboard.spec.ts +++ b/tests/e2e/responsive-dashboard.spec.ts @@ -11,6 +11,8 @@ const routeChecks: { { marker: /RaidGuild accounting/i, path: "/", screenshot: true }, { marker: /Current Treasury Balance/i, path: "/", role: "member", screenshot: true }, { marker: /Treasury portfolio/i, path: "/portfolio", role: "member", screenshot: true }, + { marker: /Hat Stewards/i, path: "/hat-stewards", role: "member", screenshot: true }, + { marker: /Role Safe mappings/i, path: "/hat-stewards", role: "admin" }, { marker: /Quarter Reports|Member access required/i, path: "/reports", role: "member" }, { marker: /Proposal-linked money movement/i, path: "/proposals", role: "member", screenshot: true }, { marker: /Joins, dues, and ragequits/i, path: "/membership", role: "member", screenshot: true }, From 8d21d490f49750c92651bb9cca7e6abaad44551f Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 6 Jul 2026 03:09:24 +0000 Subject: [PATCH 2/2] fix: address hat stewards review feedback --- src/app/hat-stewards/actions.ts | 50 +++++++- .../hat-stewards/hat-steward-admin-form.tsx | 118 ++++++++++++++++-- src/lib/hat-stewards/hats.ts | 2 +- 3 files changed, 157 insertions(+), 13 deletions(-) diff --git a/src/app/hat-stewards/actions.ts b/src/app/hat-stewards/actions.ts index 6ffc285..2e23bcb 100644 --- a/src/app/hat-stewards/actions.ts +++ b/src/app/hat-stewards/actions.ts @@ -2,7 +2,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; -import { eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import { gnosis } from "viem/chains"; import { getDb } from "@/db"; @@ -18,7 +18,7 @@ import { const HAT_STEWARDS_PATH = "/hat-stewards"; export type HatStewardRoleSafeFormState = { - field?: "safeAddress"; + field?: "hatId" | "safeAddress"; message: string; }; @@ -45,7 +45,9 @@ async function getSelectedHat(hatId: string) { throw new Error("Hats list is unavailable"); } - const hat = hats.hats.find((option) => option.prettyId === hatId); + const hat = hats.hats.find( + (option) => option.id === hatId || option.prettyId === hatId, + ); if (!hat) { throw new Error("Selected hat was not found in the RaidGuild tree"); @@ -61,7 +63,7 @@ async function getHatFromForm(formData: FormData) { const hat = await getSelectedHat(hatId); return { - id: hat.prettyId, + id: hat.id, label: hat.label, prettyId: hat.prettyId, }; @@ -85,6 +87,31 @@ async function getHatFromForm(formData: FormData) { }; } +async function getConflictingHatStewardRoleSafe(hatId: string, ignoredId?: string) { + const [existing] = await getDb() + .select({ id: hatStewardRoleSafes.id }) + .from(hatStewardRoleSafes) + .where( + ignoredId + ? and(eq(hatStewardRoleSafes.hatId, hatId), ne(hatStewardRoleSafes.id, ignoredId)) + : eq(hatStewardRoleSafes.hatId, hatId), + ) + .limit(1); + + return existing ?? null; +} + +async function assertHatIsAvailable(hatId: string, ignoredId?: string) { + const conflictingRoleSafe = await getConflictingHatStewardRoleSafe( + hatId, + ignoredId, + ); + + if (conflictingRoleSafe) { + throw new Error("Hat is already assigned to another Steward Safe"); + } +} + export async function saveHatStewardRoleSafe(formData: FormData) { const session = await requireAdminSession(); const id = getString(formData, "id"); @@ -94,6 +121,8 @@ export async function saveHatStewardRoleSafe(formData: FormData) { const db = getDb(); if (id) { + await assertHatIsAvailable(hat.id, id); + await db .update(hatStewardRoleSafes) .set({ @@ -115,6 +144,7 @@ export async function saveHatStewardRoleSafe(formData: FormData) { summary: "Updated Hat Steward role Safe", }); } else { + const existingRoleSafe = await getConflictingHatStewardRoleSafe(hat.id); const [roleSafe] = await db .insert(hatStewardRoleSafes) .values({ @@ -139,7 +169,7 @@ export async function saveHatStewardRoleSafe(formData: FormData) { .returning(); await writeAuditEvent({ - action: "update", + action: existingRoleSafe ? "update" : "create", actorWalletAddress: session.address, metadata: { hatPrettyId: hat.prettyId }, subjectId: roleSafe.id, @@ -169,6 +199,16 @@ export async function saveHatStewardRoleSafeWithState( }; } + if ( + error instanceof Error && + error.message === "Hat is already assigned to another Steward Safe" + ) { + return { + field: "hatId", + message: "That Hat is already assigned to another Steward Safe.", + }; + } + throw error; } diff --git a/src/app/hat-stewards/hat-steward-admin-form.tsx b/src/app/hat-stewards/hat-steward-admin-form.tsx index 9459e52..4f01645 100644 --- a/src/app/hat-stewards/hat-steward-admin-form.tsx +++ b/src/app/hat-stewards/hat-steward-admin-form.tsx @@ -2,7 +2,15 @@ import { Archive, Plus, RotateCcw, Save, ShieldCheck } from "lucide-react"; import Link from "next/link"; -import { type ReactNode, useActionState, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + type ReactNode, + useActionState, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { archiveHatStewardRoleSafe, @@ -30,6 +38,76 @@ function ModalShell({ children: ReactNode; title: string; }) { + const dialogRef = useRef(null); + const router = useRouter(); + + useEffect(() => { + const previouslyFocusedElement = + document.activeElement instanceof HTMLElement ? document.activeElement : null; + const dialog = dialogRef.current; + + dialog + ?.querySelector( + "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])", + ) + ?.focus(); + + return () => { + previouslyFocusedElement?.focus(); + }; + }, []); + + useEffect(() => { + function closeModal() { + router.replace("/hat-stewards", { scroll: false }); + } + + function handleKeyDown(event: KeyboardEvent) { + const dialog = dialogRef.current; + + if (!dialog) { + return; + } + + if (event.key === "Escape") { + event.preventDefault(); + closeModal(); + return; + } + + if (event.key !== "Tab") { + return; + } + + const focusableElements = Array.from( + dialog.querySelectorAll( + "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])", + ), + ).filter((element) => element.offsetParent !== null); + + if (focusableElements.length === 0) { + event.preventDefault(); + dialog.focus(); + return; + } + + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + if (event.shiftKey && document.activeElement === firstElement) { + event.preventDefault(); + lastElement.focus(); + } else if (!event.shiftKey && document.activeElement === lastElement) { + event.preventDefault(); + firstElement.focus(); + } + } + + document.addEventListener("keydown", handleKeyDown); + + return () => document.removeEventListener("keydown", handleKeyDown); + }, [router]); + return (
@@ -103,12 +183,14 @@ export function HatStewardAdminForm({ roleSafes: HatStewardRoleSafe[]; }) { const [query, setQuery] = useState(""); - const [formState, formAction] = useActionState( + const [formState, formAction, isPending] = useActionState( saveHatStewardRoleSafeWithState, EMPTY_FORM_STATE, ); const activeMappings = new Set( - roleSafes.flatMap((roleSafe) => [roleSafe.hatId, roleSafe.hatPrettyId]), + roleSafes + .filter((roleSafe) => !roleSafe.archivedAt) + .flatMap((roleSafe) => [roleSafe.hatId, roleSafe.hatPrettyId]), ); const hats = hatOptions.hats; const hatsReady = hatOptions.status === "ready"; @@ -179,6 +261,16 @@ export function HatStewardAdminForm({
) : ( @@ -267,10 +370,11 @@ export function HatStewardAdminForm({ diff --git a/src/lib/hat-stewards/hats.ts b/src/lib/hat-stewards/hats.ts index d955ab9..9374132 100644 --- a/src/lib/hat-stewards/hats.ts +++ b/src/lib/hat-stewards/hats.ts @@ -290,7 +290,7 @@ export async function listHatOptions(): Promise { treeHats, METADATA_FETCH_CONCURRENCY, async (hat) => { - const prettyId = getDecimalPrettyId(hat.id); + const prettyId = hat.prettyId || getDecimalPrettyId(hat.id); return { currentSupply: Number(hat.currentSupply),