diff --git a/apps/chain-indexer/drizzle/0007_secret_overlord.sql b/apps/chain-indexer/drizzle/0007_secret_overlord.sql new file mode 100644 index 0000000000..f84d5497bc --- /dev/null +++ b/apps/chain-indexer/drizzle/0007_secret_overlord.sql @@ -0,0 +1,24 @@ +CREATE TABLE "akash"."provider_audit_signatures" ( + "owner_account_id" integer NOT NULL, + "auditor_account_id" integer NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "height" bigint NOT NULL, + CONSTRAINT "provider_audit_signatures_owner_account_id_auditor_account_id_key_pk" PRIMARY KEY("owner_account_id","auditor_account_id","key") +); +--> statement-breakpoint +CREATE TABLE "akash"."providers" ( + "owner_account_id" integer PRIMARY KEY NOT NULL, + "host_uri" text NOT NULL, + "email" text, + "website" text, + "attributes" jsonb NOT NULL, + "last_processed_height" bigint NOT NULL, + "created_height" bigint NOT NULL, + "updated_height" bigint, + "deleted_height" bigint +); +--> statement-breakpoint +ALTER TABLE "akash"."provider_audit_signatures" ADD CONSTRAINT "provider_audit_signatures_owner_account_id_accounts_id_fk" FOREIGN KEY ("owner_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "akash"."provider_audit_signatures" ADD CONSTRAINT "provider_audit_signatures_auditor_account_id_accounts_id_fk" FOREIGN KEY ("auditor_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "akash"."providers" ADD CONSTRAINT "providers_owner_account_id_accounts_id_fk" FOREIGN KEY ("owner_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/apps/chain-indexer/drizzle/meta/0007_snapshot.json b/apps/chain-indexer/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000000..cc6c10eb4e --- /dev/null +++ b/apps/chain-indexer/drizzle/meta/0007_snapshot.json @@ -0,0 +1,2321 @@ +{ + "id": "a2860b14-74a0-48d3-825f-dc6cecbee0d3", + "prevId": "7703e3c3-9988-452e-ae30-44e28b863168", + "version": "7", + "dialect": "postgresql", + "tables": { + "cosmos.account_balances": { + "name": "account_balances", + "schema": "cosmos", + "columns": { + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_balances_account_id_accounts_id_fk": { + "name": "account_balances_account_id_accounts_id_fk", + "tableFrom": "account_balances", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_balances_account_id_denom_pk": { + "name": "account_balances_account_id_denom_pk", + "columns": [ + "account_id", + "denom" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.account_txs": { + "name": "account_txs", + "schema": "cosmos", + "columns": { + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "account_tx_role", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_txs_account_id_accounts_id_fk": { + "name": "account_txs_account_id_accounts_id_fk", + "tableFrom": "account_txs", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_txs_account_id_height_tx_index_role_pk": { + "name": "account_txs_account_id_height_tx_index_role_pk", + "columns": [ + "account_id", + "height", + "tx_index", + "role" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.accounts": { + "name": "accounts", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_number": { + "name": "account_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_module_account": { + "name": "is_module_account", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "accounts_address_idx": { + "name": "accounts_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.balance_changes": { + "name": "balance_changes", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delta": { + "name": "delta", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance_after": { + "name": "balance_after", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "balance_change_reason", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "event_index": { + "name": "event_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "counterparty_account_id": { + "name": "counterparty_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "balance_changes_account_denom_height_idx": { + "name": "balance_changes_account_denom_height_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "denom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "balance_changes_height_event_index_idx": { + "name": "balance_changes_height_event_index_idx", + "columns": [ + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "balance_changes_account_id_accounts_id_fk": { + "name": "balance_changes_account_id_accounts_id_fk", + "tableFrom": "balance_changes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "balance_changes_counterparty_account_id_accounts_id_fk": { + "name": "balance_changes_counterparty_account_id_accounts_id_fk", + "tableFrom": "balance_changes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "counterparty_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.bids": { + "name": "bids", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "oseq": { + "name": "oseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bseq": { + "name": "bseq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bid_state", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bids_deployment_id_deployments_id_fk": { + "name": "bids_deployment_id_deployments_id_fk", + "tableFrom": "bids", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bids_provider_account_id_accounts_id_fk": { + "name": "bids_provider_account_id_accounts_id_fk", + "tableFrom": "bids", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "provider_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bids_deployment_id_gseq_oseq_bseq_provider_account_id_pk": { + "name": "bids_deployment_id_gseq_oseq_bseq_provider_account_id_pk", + "columns": [ + "deployment_id", + "gseq", + "oseq", + "bseq", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.blocks": { + "name": "blocks", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "datetime": { + "name": "datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "parent_hash": { + "name": "parent_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "proposer_address": { + "name": "proposer_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_count": { + "name": "tx_count", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.delegations": { + "name": "delegations", + "schema": "cosmos", + "columns": { + "delegator_account_id": { + "name": "delegator_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validator_operator_address": { + "name": "validator_operator_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "delegations_delegator_account_id_accounts_id_fk": { + "name": "delegations_delegator_account_id_accounts_id_fk", + "tableFrom": "delegations", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "delegator_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "delegations_delegator_account_id_validator_operator_address_pk": { + "name": "delegations_delegator_account_id_validator_operator_address_pk", + "columns": [ + "delegator_account_id", + "validator_operator_address" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_events": { + "name": "deployment_events", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "msg_index": { + "name": "msg_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "deployment_event_type", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_events_deployment_id_deployments_id_fk": { + "name": "deployment_events_deployment_id_deployments_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_deployment_id_height_ordinal_pk": { + "name": "deployment_events_deployment_id_height_ordinal_pk", + "columns": [ + "deployment_id", + "height", + "ordinal" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_group_resources": { + "name": "deployment_group_resources", + "schema": "akash", + "columns": { + "deployment_group_id": { + "name": "deployment_group_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idx": { + "name": "idx", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_vendor": { + "name": "gpu_vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gpu_model": { + "name": "gpu_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "price_denom": { + "name": "price_denom", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_group_resources_deployment_group_id_deployment_groups_id_fk": { + "name": "deployment_group_resources_deployment_group_id_deployment_groups_id_fk", + "tableFrom": "deployment_group_resources", + "tableTo": "deployment_groups", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_group_resources_deployment_group_id_idx_pk": { + "name": "deployment_group_resources_deployment_group_id_idx_pk", + "columns": [ + "deployment_group_id", + "idx" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_groups": { + "name": "deployment_groups", + "schema": "akash", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "group_state", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_groups_deployment_gseq_idx": { + "name": "deployment_groups_deployment_gseq_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gseq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_groups_deployment_id_deployments_id_fk": { + "name": "deployment_groups_deployment_id_deployments_id_fk", + "tableFrom": "deployment_groups", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployments": { + "name": "deployments", + "schema": "akash", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dseq": { + "name": "dseq", + "type": "numeric(20, 0)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deposit": { + "name": "deposit", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "block_rate": { + "name": "block_rate", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_withdraw_height": { + "name": "last_withdraw_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_processed_height": { + "name": "last_processed_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "deployment_close_reason", + "typeSchema": "akash", + "primaryKey": false, + "notNull": false + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployments_owner_dseq_idx": { + "name": "deployments_owner_dseq_idx", + "columns": [ + { + "expression": "owner_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dseq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployments_owner_created_idx": { + "name": "deployments_owner_created_idx", + "columns": [ + { + "expression": "owner_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployments_open_idx": { + "name": "deployments_open_idx", + "columns": [ + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"akash\".\"deployments\".\"closed_height\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owner_account_id_accounts_id_fk": { + "name": "deployments_owner_account_id_accounts_id_fk", + "tableFrom": "deployments", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.indexer_state": { + "name": "indexer_state", + "schema": "", + "columns": { + "stream": { + "name": "stream", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_height": { + "name": "last_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.leases": { + "name": "leases", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deployment_group_id": { + "name": "deployment_group_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "oseq": { + "name": "oseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bseq": { + "name": "bseq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "predicted_closed_height": { + "name": "predicted_closed_height", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "leases_provider_idx": { + "name": "leases_provider_idx", + "columns": [ + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "closed_height", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leases_open_idx": { + "name": "leases_open_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"akash\".\"leases\".\"closed_height\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leases_deployment_id_deployments_id_fk": { + "name": "leases_deployment_id_deployments_id_fk", + "tableFrom": "leases", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "leases_deployment_group_id_deployment_groups_id_fk": { + "name": "leases_deployment_group_id_deployment_groups_id_fk", + "tableFrom": "leases", + "tableTo": "deployment_groups", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "leases_provider_account_id_accounts_id_fk": { + "name": "leases_provider_account_id_accounts_id_fk", + "tableFrom": "leases", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "provider_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "leases_deployment_id_gseq_oseq_bseq_provider_account_id_pk": { + "name": "leases_deployment_id_gseq_oseq_bseq_provider_account_id_pk", + "columns": [ + "deployment_id", + "gseq", + "oseq", + "bseq", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.message_dead_letters": { + "name": "message_dead_letters", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_dead_letters_type_id_idx": { + "name": "message_dead_letters_type_id_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_dead_letters_type_id_message_types_id_fk": { + "name": "message_dead_letters_type_id_message_types_id_fk", + "tableFrom": "message_dead_letters", + "tableTo": "message_types", + "schemaTo": "cosmos", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_dead_letters_height_tx_index_index_pk": { + "name": "message_dead_letters_height_tx_index_index_pk", + "columns": [ + "height", + "tx_index", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.message_types": { + "name": "message_types", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_types_type_idx": { + "name": "message_types_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.messages": { + "name": "messages", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_type_id_idx": { + "name": "messages_type_id_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_type_id_message_types_id_fk": { + "name": "messages_type_id_message_types_id_fk", + "tableFrom": "messages", + "tableTo": "message_types", + "schemaTo": "cosmos", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "messages_height_tx_index_index_pk": { + "name": "messages_height_tx_index_index_pk", + "columns": [ + "height", + "tx_index", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposal_deposits": { + "name": "proposal_deposits", + "schema": "cosmos", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "depositor_account_id": { + "name": "depositor_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposal_deposits_depositor_account_id_accounts_id_fk": { + "name": "proposal_deposits_depositor_account_id_accounts_id_fk", + "tableFrom": "proposal_deposits", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "depositor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proposal_deposits_proposal_id_depositor_account_id_height_pk": { + "name": "proposal_deposits_proposal_id_depositor_account_id_height_pk", + "columns": [ + "proposal_id", + "depositor_account_id", + "height" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposal_votes": { + "name": "proposal_votes", + "schema": "cosmos", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "voter_account_id": { + "name": "voter_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposal_votes_voter_account_id_accounts_id_fk": { + "name": "proposal_votes_voter_account_id_accounts_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "voter_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proposal_votes_proposal_id_voter_account_id_pk": { + "name": "proposal_votes_proposal_id_voter_account_id_pk", + "columns": [ + "proposal_id", + "voter_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposals": { + "name": "proposals", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "proposer_account_id": { + "name": "proposer_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "proposal_status", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + }, + "submit_time": { + "name": "submit_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deposit_end_time": { + "name": "deposit_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voting_start_time": { + "name": "voting_start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voting_end_time": { + "name": "voting_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_deposit": { + "name": "total_deposit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_tally_yes": { + "name": "final_tally_yes", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_abstain": { + "name": "final_tally_abstain", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_no": { + "name": "final_tally_no", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_no_with_veto": { + "name": "final_tally_no_with_veto", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "submit_height": { + "name": "submit_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposals_proposer_account_id_accounts_id_fk": { + "name": "proposals_proposer_account_id_accounts_id_fk", + "tableFrom": "proposals", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "proposer_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.provider_audit_signatures": { + "name": "provider_audit_signatures", + "schema": "akash", + "columns": { + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auditor_account_id": { + "name": "auditor_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "provider_audit_signatures_owner_account_id_accounts_id_fk": { + "name": "provider_audit_signatures_owner_account_id_accounts_id_fk", + "tableFrom": "provider_audit_signatures", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "provider_audit_signatures_auditor_account_id_accounts_id_fk": { + "name": "provider_audit_signatures_auditor_account_id_accounts_id_fk", + "tableFrom": "provider_audit_signatures", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "auditor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "provider_audit_signatures_owner_account_id_auditor_account_id_key_pk": { + "name": "provider_audit_signatures_owner_account_id_auditor_account_id_key_pk", + "columns": [ + "owner_account_id", + "auditor_account_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.providers": { + "name": "providers", + "schema": "akash", + "columns": { + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "host_uri": { + "name": "host_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_processed_height": { + "name": "last_processed_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_height": { + "name": "updated_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_height": { + "name": "deleted_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "providers_owner_account_id_accounts_id_fk": { + "name": "providers_owner_account_id_accounts_id_fk", + "tableFrom": "providers", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.transactions": { + "name": "transactions", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "gas_used": { + "name": "gas_used", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gas_wanted": { + "name": "gas_wanted", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "fee": { + "name": "fee", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "transactions_hash_idx": { + "name": "transactions_hash_idx", + "columns": [ + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "transactions_height_index_pk": { + "name": "transactions_height_index_pk", + "columns": [ + "height", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.unbonding_delegations": { + "name": "unbonding_delegations", + "schema": "cosmos", + "columns": { + "delegator_account_id": { + "name": "delegator_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validator_operator_address": { + "name": "validator_operator_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_height": { + "name": "creation_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "completion_time": { + "name": "completion_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "initial_balance": { + "name": "initial_balance", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "unbonding_delegations_delegator_account_id_accounts_id_fk": { + "name": "unbonding_delegations_delegator_account_id_accounts_id_fk", + "tableFrom": "unbonding_delegations", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "delegator_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "unbonding_delegations_delegator_account_id_validator_operator_address_creation_height_pk": { + "name": "unbonding_delegations_delegator_account_id_validator_operator_address_creation_height_pk", + "columns": [ + "delegator_account_id", + "validator_operator_address", + "creation_height" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.validators": { + "name": "validators", + "schema": "cosmos", + "columns": { + "operator_address": { + "name": "operator_address", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hex_address": { + "name": "hex_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moniker": { + "name": "moniker", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity": { + "name": "identity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security_contact": { + "name": "security_contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commission_rate": { + "name": "commission_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "commission_max_rate": { + "name": "commission_max_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "commission_max_change_rate": { + "name": "commission_max_change_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "min_self_delegation": { + "name": "min_self_delegation", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "jailed": { + "name": "jailed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "validator_status", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "delegator_shares": { + "name": "delegator_shares", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": false + }, + "unbonding_height": { + "name": "unbonding_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "unbonding_time": { + "name": "unbonding_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "cosmos.account_tx_role": { + "name": "account_tx_role", + "schema": "cosmos", + "values": [ + "signer", + "sender", + "receiver" + ] + }, + "cosmos.balance_change_reason": { + "name": "balance_change_reason", + "schema": "cosmos", + "values": [ + "genesis", + "transfer", + "fee", + "reward", + "commission", + "slash", + "gov", + "ibc", + "escrow", + "bme", + "mint", + "burn", + "staking" + ] + }, + "akash.bid_state": { + "name": "bid_state", + "schema": "akash", + "values": [ + "open", + "active", + "closed" + ] + }, + "akash.deployment_close_reason": { + "name": "deployment_close_reason", + "schema": "akash", + "values": [ + "close_message", + "overdrawn", + "close_event" + ] + }, + "akash.deployment_event_type": { + "name": "deployment_event_type", + "schema": "akash", + "values": [ + "created", + "deposited", + "updated", + "closed", + "group_closed", + "group_paused", + "group_started", + "bid_created", + "bid_closed", + "lease_created", + "lease_closed", + "lease_withdrawn" + ] + }, + "akash.group_state": { + "name": "group_state", + "schema": "akash", + "values": [ + "open", + "paused", + "closed" + ] + }, + "cosmos.proposal_status": { + "name": "proposal_status", + "schema": "cosmos", + "values": [ + "deposit_period", + "voting_period", + "passed", + "rejected", + "failed" + ] + }, + "cosmos.validator_status": { + "name": "validator_status", + "schema": "cosmos", + "values": [ + "unbonded", + "unbonding", + "bonded" + ] + }, + "cosmos.vote_option": { + "name": "vote_option", + "schema": "cosmos", + "values": [ + "yes", + "abstain", + "no", + "no_with_veto" + ] + } + }, + "schemas": { + "akash": "akash", + "cosmos": "cosmos" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/chain-indexer/drizzle/meta/_journal.json b/apps/chain-indexer/drizzle/meta/_journal.json index eda4b50e95..dc53cc0966 100644 --- a/apps/chain-indexer/drizzle/meta/_journal.json +++ b/apps/chain-indexer/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1786877216729, "tag": "0006_next_earthquake", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1786898959325, + "tag": "0007_secret_overlord", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/chain-indexer/src/akash/akash-changes.spec.ts b/apps/chain-indexer/src/akash/akash-changes.spec.ts new file mode 100644 index 0000000000..55ca1ef94f --- /dev/null +++ b/apps/chain-indexer/src/akash/akash-changes.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import type { AkashBlockChanges, AkashChangeBody } from "@src/akash/akash-changes"; +import { collectAkashAddresses, isProviderChange } from "@src/akash/akash-changes"; + +const BLOCK_TIME = new Date("2026-08-13T00:00:00Z"); + +describe("collectAkashAddresses", () => { + it("collects owners, providers and depositors from deployment-keyed changes", () => { + const addresses = collectAkashAddresses([ + block([ + { kind: "deploymentCreated", key: { owner: "akash1owner", dseq: "1" }, denom: "uakt", deposit: "1", depositor: "akash1depositor", groups: [] }, + { kind: "bidCreated", key: { owner: "akash1owner", dseq: "1", gseq: 1, oseq: 1, bseq: 0, provider: "akash1bidder" }, price: "1", priceDenom: "uakt" } + ]) + ]); + + expect(addresses).toEqual(new Set(["akash1owner", "akash1depositor", "akash1bidder"])); + }); + + it("collects the owner and auditor from provider and audit changes", () => { + const addresses = collectAkashAddresses([ + block([ + { kind: "providerCreated", owner: "akash1prov", hostUri: "https://x", email: null, website: null, attributes: [] }, + { kind: "providerDeleted", owner: "akash1gone" }, + { kind: "providerAttributesSigned", owner: "akash1prov", auditor: "akash1auditor", attributes: [] }, + { kind: "providerAttributesUnsigned", owner: "akash1prov", auditor: "akash1revoker", keys: [] } + ]) + ]); + + expect(addresses).toEqual(new Set(["akash1prov", "akash1gone", "akash1auditor", "akash1revoker"])); + }); +}); + +describe("isProviderChange", () => { + it("narrows provider and audit kinds and rejects deployment-keyed ones", () => { + const provider: AkashChangeBody = { kind: "providerDeleted", owner: "akash1prov" }; + const deployment: AkashChangeBody = { kind: "deploymentClosed", key: { owner: "akash1owner", dseq: "1" } }; + + expect(isProviderChange({ ...provider, txIndex: 0, msgIndex: 0 })).toBe(true); + expect(isProviderChange({ ...deployment, txIndex: 0, msgIndex: 0 })).toBe(false); + }); +}); + +function block(bodies: AkashChangeBody[]): AkashBlockChanges { + return { height: 100, datetime: BLOCK_TIME, changes: bodies.map((body, index) => ({ ...body, txIndex: 0, msgIndex: index })) }; +} diff --git a/apps/chain-indexer/src/akash/akash-changes.ts b/apps/chain-indexer/src/akash/akash-changes.ts index ad34fe836c..c282358de5 100644 --- a/apps/chain-indexer/src/akash/akash-changes.ts +++ b/apps/chain-indexer/src/akash/akash-changes.ts @@ -41,7 +41,21 @@ interface ChangeOrigin { msgIndex: number | null; } +export interface ProviderAttribute { + key: string; + value: string; +} + +export type ProviderChangeBody = + | { kind: "providerCreated"; owner: string; hostUri: string; email: string | null; website: string | null; attributes: ProviderAttribute[] } + | { kind: "providerUpdated"; owner: string; hostUri: string; email: string | null; website: string | null; attributes: ProviderAttribute[] } + | { kind: "providerDeleted"; owner: string } + | { kind: "providerAttributesSigned"; owner: string; auditor: string; attributes: ProviderAttribute[] } + /** Empty `keys` means the auditor revoked every attribute they signed for this provider. */ + | { kind: "providerAttributesUnsigned"; owner: string; auditor: string; keys: string[] }; + export type AkashChangeBody = + | ProviderChangeBody | { kind: "deploymentCreated"; key: DeploymentKey; denom: string; deposit: string; depositor: string | null; groups: NormalizedGroup[] } | { kind: "deploymentDeposited"; key: DeploymentKey; amount: string; depositor: string | null } | { kind: "deploymentUpdated"; key: DeploymentKey } @@ -61,6 +75,26 @@ export type AkashChange = AkashChangeBody & ChangeOrigin; export type AkashChangeKind = AkashChange["kind"]; +export type ProviderChange = ProviderChangeBody & ChangeOrigin; + +const PROVIDER_REGISTRY_KINDS = new Set(["providerCreated", "providerUpdated", "providerDeleted"]); +const PROVIDER_AUDIT_KINDS = new Set(["providerAttributesSigned", "providerAttributesUnsigned"]); + +export type ProviderRegistryChange = Extract; +export type ProviderAuditChange = Extract; + +export function isProviderRegistryChange(change: AkashChange): change is ProviderRegistryChange { + return PROVIDER_REGISTRY_KINDS.has(change.kind); +} + +export function isProviderAuditChange(change: AkashChange): change is ProviderAuditChange { + return PROVIDER_AUDIT_KINDS.has(change.kind); +} + +export function isProviderChange(change: AkashChange): change is ProviderChange { + return isProviderRegistryChange(change) || isProviderAuditChange(change); +} + /** Everything derived from one block, in the exact order the chain applied it (tx order, then message order, then that tx's close events). */ export interface AkashBlockChanges { height: number; @@ -74,6 +108,13 @@ export function collectAkashAddresses(blocks: AkashBlockChanges[]): Set for (const block of blocks) { for (const change of block.changes) { + if (isProviderChange(change)) { + addresses.add(change.owner); + if ("auditor" in change) { + addresses.add(change.auditor); + } + continue; + } addresses.add(change.key.owner); if ("provider" in change) { addresses.add(change.provider); diff --git a/apps/chain-indexer/src/akash/akash-deriver.spec.ts b/apps/chain-indexer/src/akash/akash-deriver.spec.ts index b29628df0e..eb14ee295c 100644 --- a/apps/chain-indexer/src/akash/akash-deriver.spec.ts +++ b/apps/chain-indexer/src/akash/akash-deriver.spec.ts @@ -30,6 +30,76 @@ describe("deriveAkashChanges", () => { ]); }); + it("derives provider and audit changes alongside deployment ones", () => { + const changes = deriveAkashChanges( + block({ + messages: [ + { + typeUrl: "/akash.provider.v1beta4.MsgCreateProvider", + body: { owner: "akash1owner", hostUri: "https://provider.example.com:8443", attributes: [{ key: "region", value: "us-west" }], info: {} } + }, + { + typeUrl: "/akash.audit.v1.MsgSignProviderAttributes", + body: { owner: "akash1owner", auditor: "akash1auditor", attributes: [{ key: "region", value: "us-west" }] } + } + ] + }) + ); + + expect(changes.changes).toEqual([ + { + kind: "providerCreated", + owner: "akash1owner", + hostUri: "https://provider.example.com:8443", + email: null, + website: null, + attributes: [{ key: "region", value: "us-west" }], + txIndex: 0, + msgIndex: 0 + }, + { + kind: "providerAttributesSigned", + owner: "akash1owner", + auditor: "akash1auditor", + attributes: [{ key: "region", value: "us-west" }], + txIndex: 0, + msgIndex: 1 + } + ]); + }); + + it("unwraps an authz-wrapped audit sign", () => { + const changes = deriveAkashChanges( + block({ + messages: [ + { + typeUrl: "/cosmos.authz.v1beta1.MsgExec", + body: { + grantee: "akash1grantee", + msgs: [ + { + typeUrl: "/akash.audit.v1beta3.MsgSignProviderAttributes", + decoded: { owner: "akash1owner", auditor: "akash1auditor", attributes: [{ key: "tier", value: "community" }] } + } + ] + } + } + ] + }) + ); + + expect(changes.changes).toEqual([ + { + kind: "providerAttributesSigned", + owner: "akash1owner", + auditor: "akash1auditor", + attributes: [{ key: "tier", value: "community" }], + txIndex: 0, + msgIndex: 0 + } + ]); + }); + it("skips messages in failed transactions", () => { const changes = deriveAkashChanges( block({ diff --git a/apps/chain-indexer/src/akash/akash-deriver.ts b/apps/chain-indexer/src/akash/akash-deriver.ts index 6f600ce495..d69315550c 100644 --- a/apps/chain-indexer/src/akash/akash-deriver.ts +++ b/apps/chain-indexer/src/akash/akash-deriver.ts @@ -1,7 +1,9 @@ import type { AkashBlockChanges, AkashChange, AkashChangeBody } from "@src/akash/akash-changes"; import { asInteger, asRecord, asString } from "@src/akash/json"; +import { normalizeAuditMessage } from "@src/akash/normalize-audit"; import { normalizeDeploymentMessage } from "@src/akash/normalize-deployment"; import { normalizeMarketMessage } from "@src/akash/normalize-market"; +import { normalizeProviderMessage } from "@src/akash/normalize-provider"; import { asUint64String } from "@src/akash/uint64"; import type { DecodedBlock, DecodedEvent } from "@src/pipeline/decoded-block"; import { MAX_EXEC_DEPTH, MSG_EXEC_TYPE_URL } from "@src/pipeline/msg-exec"; @@ -11,7 +13,7 @@ const DEPLOYMENT_CLOSED_EVENT_TYPE = "akash.deployment.v1.EventDeploymentClosed" const LEASE_CLOSED_EVENT_TYPE = "akash.market.v1.EventLeaseClosed"; /** - * Extracts the deployment and market lifecycle from a block's messages and close events, in the exact + * Extracts the deployment, market, provider and audit lifecycle from a block's messages and close events, in the exact * order the chain applied it: per transaction, messages first (authz MsgExec unwrapped through the * decoder-provided `decoded` field), then that transaction's close events, which catch deployment and * lease closes happening as side effects (group close, authz revoke, overdraw on withdraw). Messages @@ -56,7 +58,11 @@ function addMessage(changes: AkashChange[], typeUrl: string, body: unknown, txIn return; } - const normalized = normalizeDeploymentMessage(typeUrl, record) ?? normalizeMarketMessage(typeUrl, record); + const normalized = + normalizeDeploymentMessage(typeUrl, record) ?? + normalizeMarketMessage(typeUrl, record) ?? + normalizeProviderMessage(typeUrl, record) ?? + normalizeAuditMessage(typeUrl, record); if (normalized) { changes.push({ ...normalized, txIndex, msgIndex }); diff --git a/apps/chain-indexer/src/akash/akash-writer.service.spec.ts b/apps/chain-indexer/src/akash/akash-writer.service.spec.ts index 2f332f3321..fa1e0fb5d9 100644 --- a/apps/chain-indexer/src/akash/akash-writer.service.spec.ts +++ b/apps/chain-indexer/src/akash/akash-writer.service.spec.ts @@ -95,6 +95,20 @@ describe(AkashWriter.name, () => { expect(inserts).toEqual([]); }); + it("ignores provider changes entirely", async () => { + const { writer, tx, inserts, selects, logger } = setup(); + + await writer.write( + tx, + [block(100, [{ kind: "providerCreated", owner: OWNER, hostUri: "https://x", email: null, website: null, attributes: [] }])], + ACCOUNT_IDS + ); + + expect(inserts).toEqual([]); + expect(selects).toEqual([]); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it("logs orphan references without aborting the batch", async () => { const { writer, tx, logger, inserts } = setup(); diff --git a/apps/chain-indexer/src/akash/akash-writer.service.ts b/apps/chain-indexer/src/akash/akash-writer.service.ts index 1471145dd1..605f4e699b 100644 --- a/apps/chain-indexer/src/akash/akash-writer.service.ts +++ b/apps/chain-indexer/src/akash/akash-writer.service.ts @@ -3,6 +3,7 @@ import groupBy from "lodash/groupBy"; import { inject, singleton } from "tsyringe"; import type { AkashBlockChanges, DeploymentKey, NormalizedResource } from "@src/akash/akash-changes"; +import { isProviderChange } from "@src/akash/akash-changes"; import { decFromString, decToString } from "@src/akash/dec"; import type { BidStateValue, DeploymentAggState, GroupStateValue, ReducerWarning } from "@src/akash/deployment-reducer"; import { applyBlockChanges, stateKey } from "@src/akash/deployment-reducer"; @@ -42,6 +43,9 @@ export class AkashWriter { } const keyed = this.#collectKeys(withChanges, accountIds); + if (keyed.length === 0) { + return; + } const { states, deploymentIds, groupIds, loadedAddressIds } = await this.#loadStates(tx, keyed); const warnings = withChanges.flatMap(block => applyBlockChanges(states, block)); @@ -68,6 +72,9 @@ export class AkashWriter { const byKey = new Map(); for (const block of blocks) { for (const change of block.changes) { + if (isProviderChange(change)) { + continue; + } byKey.set(stateKey(change.key), change.key); } } diff --git a/apps/chain-indexer/src/akash/deployment-reducer.spec.ts b/apps/chain-indexer/src/akash/deployment-reducer.spec.ts index 6ebc1dd3dd..c24ffbec13 100644 --- a/apps/chain-indexer/src/akash/deployment-reducer.spec.ts +++ b/apps/chain-indexer/src/akash/deployment-reducer.spec.ts @@ -166,6 +166,15 @@ describe("applyBlockChanges", () => { expect(warnings).toEqual([{ code: "AKASH_ORPHAN_REFERENCE", kind: "deploymentDeposited", owner: OWNER, dseq: "42", height: 100 }]); }); + it("ignores provider changes without warning or mutating state", () => { + const { states } = setup(); + + const warnings = applyBlockChanges(states, block(100, [{ kind: "providerDeleted", owner: OWNER }])); + + expect(states.size).toBe(0); + expect(warnings).toEqual([]); + }); + it("applies close-event fallbacks with settlement, only when not already closed", () => { const { states } = setup(); diff --git a/apps/chain-indexer/src/akash/deployment-reducer.ts b/apps/chain-indexer/src/akash/deployment-reducer.ts index 59809b9385..6cb1b4dea9 100644 --- a/apps/chain-indexer/src/akash/deployment-reducer.ts +++ b/apps/chain-indexer/src/akash/deployment-reducer.ts @@ -1,4 +1,5 @@ import type { AkashBlockChanges, AkashChange, DeploymentKey, LeaseSlot, NormalizedGroup, NormalizedResource } from "@src/akash/akash-changes"; +import { isProviderChange } from "@src/akash/akash-changes"; import { decCeilInt, decFromInt, decFromString, decQuo, decToString, decTruncateInt } from "@src/akash/dec"; import { normalizeDenom } from "@src/akash/denom"; import { settle, sumLeaseRate } from "@src/akash/settlement"; @@ -109,6 +110,9 @@ export function applyBlockChanges(states: Map, block const decided = new Map(); for (const change of block.changes) { + if (isProviderChange(change)) { + continue; + } const key = stateKey(change.key); if (change.kind === "deploymentCreated") { diff --git a/apps/chain-indexer/src/akash/normalize-audit.spec.ts b/apps/chain-indexer/src/akash/normalize-audit.spec.ts new file mode 100644 index 0000000000..1d5c4563fd --- /dev/null +++ b/apps/chain-indexer/src/akash/normalize-audit.spec.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeAuditMessage } from "@src/akash/normalize-audit"; + +describe("normalizeAuditMessage", () => { + it("normalizes a legacy v1beta1 sign with its attributes", () => { + const change = normalizeAuditMessage("/akash.audit.v1beta1.MsgSignProviderAttributes", { + owner: "akash1owner", + auditor: "akash1auditor", + attributes: [ + { key: "region", value: "us-west" }, + { key: "tier", value: "community" } + ] + }); + + expect(change).toEqual({ + kind: "providerAttributesSigned", + owner: "akash1owner", + auditor: "akash1auditor", + attributes: [ + { key: "region", value: "us-west" }, + { key: "tier", value: "community" } + ] + }); + }); + + it("normalizes a current v1 sign identically to the legacy eras", () => { + const change = normalizeAuditMessage("/akash.audit.v1.MsgSignProviderAttributes", { + owner: "akash1owner", + auditor: "akash1auditor", + attributes: [] + }); + + expect(change).toEqual({ kind: "providerAttributesSigned", owner: "akash1owner", auditor: "akash1auditor", attributes: [] }); + }); + + it("normalizes a keyed delete, dropping non-string keys", () => { + const change = normalizeAuditMessage("/akash.audit.v1beta3.MsgDeleteProviderAttributes", { + owner: "akash1owner", + auditor: "akash1auditor", + keys: ["region", 7, "tier"] + }); + + expect(change).toEqual({ kind: "providerAttributesUnsigned", owner: "akash1owner", auditor: "akash1auditor", keys: ["region", "tier"] }); + }); + + it("normalizes a delete without keys to an empty list meaning delete-all", () => { + const change = normalizeAuditMessage("/akash.audit.v1.MsgDeleteProviderAttributes", { owner: "akash1owner", auditor: "akash1auditor" }); + + expect(change).toEqual({ kind: "providerAttributesUnsigned", owner: "akash1owner", auditor: "akash1auditor", keys: [] }); + }); + + it("returns null when the owner or auditor is missing", () => { + expect(normalizeAuditMessage("/akash.audit.v1.MsgSignProviderAttributes", { owner: "akash1owner" })).toBeNull(); + expect(normalizeAuditMessage("/akash.audit.v1.MsgDeleteProviderAttributes", { auditor: "akash1auditor" })).toBeNull(); + }); + + it("returns null for unrelated type urls", () => { + expect(normalizeAuditMessage("/akash.audit.v1beta4.MsgSignProviderAttributes", { owner: "a", auditor: "b" })).toBeNull(); + expect(normalizeAuditMessage("/akash.provider.v1beta4.MsgCreateProvider", {})).toBeNull(); + }); +}); diff --git a/apps/chain-indexer/src/akash/normalize-audit.ts b/apps/chain-indexer/src/akash/normalize-audit.ts new file mode 100644 index 0000000000..59db22a7c3 --- /dev/null +++ b/apps/chain-indexer/src/akash/normalize-audit.ts @@ -0,0 +1,35 @@ +import type { AkashChangeBody } from "@src/akash/akash-changes"; +import { akashTypeUrlSet } from "@src/akash/akash-changes"; +import { asString } from "@src/akash/json"; +import { attributeList } from "@src/akash/resources"; + +const AUDIT_VERSIONS = ["v1beta1", "v1beta2", "v1beta3", "v1"] as const; + +const SIGN_ATTRIBUTES = typeUrlSet("MsgSignProviderAttributes"); +const DELETE_ATTRIBUTES = typeUrlSet("MsgDeleteProviderAttributes"); + +function typeUrlSet(name: string): Set { + return akashTypeUrlSet("audit", name, AUDIT_VERSIONS); +} + +export function normalizeAuditMessage(typeUrl: string, body: Record): AkashChangeBody | null { + if (SIGN_ATTRIBUTES.has(typeUrl)) { + const identity = auditIdentity(body); + return identity ? { kind: "providerAttributesSigned", ...identity, attributes: attributeList(body.attributes) } : null; + } + if (DELETE_ATTRIBUTES.has(typeUrl)) { + const identity = auditIdentity(body); + return identity ? { kind: "providerAttributesUnsigned", ...identity, keys: stringKeys(body.keys) } : null; + } + return null; +} + +function auditIdentity(body: Record): { owner: string; auditor: string } | null { + const owner = asString(body.owner); + const auditor = asString(body.auditor); + return owner && auditor ? { owner, auditor } : null; +} + +function stringKeys(keys: unknown): string[] { + return Array.isArray(keys) ? keys.filter((key): key is string => typeof key === "string") : []; +} diff --git a/apps/chain-indexer/src/akash/normalize-provider.spec.ts b/apps/chain-indexer/src/akash/normalize-provider.spec.ts new file mode 100644 index 0000000000..954d81df09 --- /dev/null +++ b/apps/chain-indexer/src/akash/normalize-provider.spec.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeProviderMessage } from "@src/akash/normalize-provider"; + +describe("normalizeProviderMessage", () => { + it("normalizes a legacy v1beta1 create with info and attributes", () => { + const change = normalizeProviderMessage("/akash.provider.v1beta1.MsgCreateProvider", { + owner: "akash1owner", + hostUri: "https://provider.example.com:8443", + attributes: [{ key: "region", value: "us-west" }], + info: { email: "ops@example.com", website: "https://example.com" } + }); + + expect(change).toEqual({ + kind: "providerCreated", + owner: "akash1owner", + hostUri: "https://provider.example.com:8443", + email: "ops@example.com", + website: "https://example.com", + attributes: [{ key: "region", value: "us-west" }] + }); + }); + + it("normalizes a current v1beta4 update identically to the legacy eras", () => { + const change = normalizeProviderMessage("/akash.provider.v1beta4.MsgUpdateProvider", { + owner: "akash1owner", + hostUri: "https://new.example.com:8443", + attributes: [], + info: { email: "", website: "" } + }); + + expect(change).toEqual({ + kind: "providerUpdated", + owner: "akash1owner", + hostUri: "https://new.example.com:8443", + email: null, + website: null, + attributes: [] + }); + }); + + it("normalizes a create without info or attributes", () => { + const change = normalizeProviderMessage("/akash.provider.v1beta3.MsgCreateProvider", { + owner: "akash1owner", + hostUri: "https://provider.example.com:8443" + }); + + expect(change).toEqual({ + kind: "providerCreated", + owner: "akash1owner", + hostUri: "https://provider.example.com:8443", + email: null, + website: null, + attributes: [] + }); + }); + + it("normalizes a delete", () => { + const change = normalizeProviderMessage("/akash.provider.v1beta2.MsgDeleteProvider", { owner: "akash1owner" }); + + expect(change).toEqual({ kind: "providerDeleted", owner: "akash1owner" }); + }); + + it("returns null when the owner or host uri is missing", () => { + expect(normalizeProviderMessage("/akash.provider.v1beta4.MsgCreateProvider", { hostUri: "https://x" })).toBeNull(); + expect(normalizeProviderMessage("/akash.provider.v1beta4.MsgUpdateProvider", { owner: "akash1owner" })).toBeNull(); + expect(normalizeProviderMessage("/akash.provider.v1beta4.MsgDeleteProvider", {})).toBeNull(); + }); + + it("returns null for unrelated type urls", () => { + expect(normalizeProviderMessage("/akash.provider.v1beta5.MsgCreateProvider", { owner: "akash1owner", hostUri: "https://x" })).toBeNull(); + expect(normalizeProviderMessage("/akash.deployment.v1beta4.MsgCreateDeployment", {})).toBeNull(); + }); +}); diff --git a/apps/chain-indexer/src/akash/normalize-provider.ts b/apps/chain-indexer/src/akash/normalize-provider.ts new file mode 100644 index 0000000000..5a1c157c8b --- /dev/null +++ b/apps/chain-indexer/src/akash/normalize-provider.ts @@ -0,0 +1,46 @@ +import type { AkashChangeBody } from "@src/akash/akash-changes"; +import { akashTypeUrlSet } from "@src/akash/akash-changes"; +import { asRecord, asString } from "@src/akash/json"; +import { attributeList } from "@src/akash/resources"; + +const PROVIDER_VERSIONS = ["v1beta1", "v1beta2", "v1beta3", "v1beta4"] as const; + +const CREATE_PROVIDER = typeUrlSet("MsgCreateProvider"); +const UPDATE_PROVIDER = typeUrlSet("MsgUpdateProvider"); +const DELETE_PROVIDER = typeUrlSet("MsgDeleteProvider"); + +function typeUrlSet(name: string): Set { + return akashTypeUrlSet("provider", name, PROVIDER_VERSIONS); +} + +/** All four provider proto eras share the same message shape, so parsing is version-independent. */ +export function normalizeProviderMessage(typeUrl: string, body: Record): AkashChangeBody | null { + if (CREATE_PROVIDER.has(typeUrl)) { + return normalizeProviderInfo(body, "providerCreated"); + } + if (UPDATE_PROVIDER.has(typeUrl)) { + return normalizeProviderInfo(body, "providerUpdated"); + } + if (DELETE_PROVIDER.has(typeUrl)) { + const owner = asString(body.owner); + return owner ? { kind: "providerDeleted", owner } : null; + } + return null; +} + +function normalizeProviderInfo(body: Record, kind: "providerCreated" | "providerUpdated"): AkashChangeBody | null { + const owner = asString(body.owner); + const hostUri = asString(body.hostUri); + if (!owner || !hostUri) { + return null; + } + const info = asRecord(body.info); + return { + kind, + owner, + hostUri, + email: asString(info?.email) || null, + website: asString(info?.website) || null, + attributes: attributeList(body.attributes) + }; +} diff --git a/apps/chain-indexer/src/akash/provider-writer.service.spec.ts b/apps/chain-indexer/src/akash/provider-writer.service.spec.ts new file mode 100644 index 0000000000..96d6d70c20 --- /dev/null +++ b/apps/chain-indexer/src/akash/provider-writer.service.spec.ts @@ -0,0 +1,309 @@ +import type { SQL } from "drizzle-orm"; +import { PgDialect } from "drizzle-orm/pg-core"; +import { describe, expect, it } from "vitest"; +import { mock } from "vitest-mock-extended"; + +import type { AkashBlockChanges, AkashChangeBody } from "@src/akash/akash-changes"; +import { ProviderWriter } from "@src/akash/provider-writer.service"; +import { ProviderAuditSignatures, Providers } from "@src/db/schema"; +import type { ChainTransaction } from "@src/providers/db.provider"; +import type { LoggerService } from "@src/providers/logging.provider"; + +const OWNER = "akash1prov"; +const AUDITOR = "akash1auditor"; +const BLOCK_TIME = new Date("2026-08-13T00:00:00Z"); +const ACCOUNT_IDS = new Map([ + [OWNER, 7], + [AUDITOR, 8] +]); + +describe(ProviderWriter.name, () => { + it("does nothing for blocks without provider changes", async () => { + const { writer, tx, inserts, deletes, selects } = setup(); + + await writer.write(tx, [block(100, [{ kind: "deploymentClosed", key: { owner: OWNER, dseq: "1" } }])], ACCOUNT_IDS); + + expect(inserts).toEqual([]); + expect(deletes).toEqual([]); + expect(selects).toEqual([]); + }); + + it("folds a create, update and delete across blocks into one guarded upsert with the final state", async () => { + const { writer, tx, inserts, upserts } = setup(); + + await writer.write( + tx, + [ + block(100, [created()]), + block(110, [updated({ hostUri: "https://new.example.com:8443", email: "new@example.com", attributes: [{ key: "tier", value: "pro" }] })]), + block(120, [{ kind: "providerDeleted", owner: OWNER }]) + ], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, Providers)).toEqual([ + { + ownerAccountId: 7, + hostUri: "https://new.example.com:8443", + email: "new@example.com", + website: null, + attributes: [{ key: "tier", value: "pro" }], + lastProcessedHeight: 120, + createdHeight: 100, + updatedHeight: 110, + deletedHeight: 120 + } + ]); + + const upsert = upserts.find(entry => entry.table === Providers); + expect(sqlText(upsert?.config.setWhere as SQL)).toContain('excluded.last_processed_height >= "akash"."providers"."last_processed_height"'); + }); + + it("resets the row when a deleted provider re-registers", async () => { + const { writer, tx, inserts } = setup({ + providers: [providerRow({ lastProcessedHeight: 120, updatedHeight: 110, deletedHeight: 120 })] + }); + + await writer.write(tx, [block(200, [created({ hostUri: "https://back.example.com:8443" })])], ACCOUNT_IDS); + + expect(rowsFor(inserts, Providers)).toEqual([ + expect.objectContaining({ + hostUri: "https://back.example.com:8443", + lastProcessedHeight: 200, + createdHeight: 200, + updatedHeight: null, + deletedHeight: null + }) + ]); + }); + + it("skips blocks at or below the stored watermark", async () => { + const { writer, tx, inserts } = setup({ providers: [providerRow({ lastProcessedHeight: 500 })] }); + + await writer.write(tx, [block(400, [created()]), block(500, [{ kind: "providerDeleted", owner: OWNER }])], ACCOUNT_IDS); + + expect(inserts).toEqual([]); + }); + + it("warns on an update for an unknown provider without writing", async () => { + const { writer, tx, logger, inserts } = setup(); + + await writer.write(tx, [block(100, [updated({})])], ACCOUNT_IDS); + + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ event: "PROVIDER_ORPHAN_REFERENCE", count: 1 })); + expect(inserts).toEqual([]); + }); + + it("upserts signed attributes deduped last-wins with a height guard", async () => { + const { writer, tx, inserts, upserts } = setup(); + + await writer.write( + tx, + [ + block(100, [ + { + kind: "providerAttributesSigned", + owner: OWNER, + auditor: AUDITOR, + attributes: [ + { key: "region", value: "us-east" }, + { key: "region", value: "us-west" }, + { key: "tier", value: "community" } + ] + } + ]) + ], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, ProviderAuditSignatures)).toEqual([ + { ownerAccountId: 7, auditorAccountId: 8, key: "region", value: "us-west", height: 100 }, + { ownerAccountId: 7, auditorAccountId: 8, key: "tier", value: "community", height: 100 } + ]); + + const upsert = upserts.find(entry => entry.table === ProviderAuditSignatures); + expect(sqlText(upsert?.config.setWhere as SQL)).toContain('excluded.height >= "akash"."provider_audit_signatures"."height"'); + }); + + it("deletes the given keys with a height guard, and all of the auditor's keys when none are given", async () => { + const { writer, tx, deletes } = setup(); + + await writer.write( + tx, + [ + block(100, [ + { kind: "providerAttributesUnsigned", owner: OWNER, auditor: AUDITOR, keys: ["region", "tier"] }, + { kind: "providerAttributesUnsigned", owner: OWNER, auditor: AUDITOR, keys: [] } + ]) + ], + ACCOUNT_IDS + ); + + expect(deletes).toHaveLength(2); + const [keyedDelete, deleteAll] = deletes.map(entry => sqlText(entry.where as SQL)); + expect(keyedDelete).toContain('"owner_account_id" = '); + expect(keyedDelete).toContain('"auditor_account_id" = '); + expect(keyedDelete).toContain('"height" <= '); + expect(keyedDelete).toContain('"key" in '); + expect(deleteAll).toContain('"height" <= '); + expect(deleteAll).not.toContain('"key" in '); + }); + + it("applies audit changes even when the provider was never registered", async () => { + const { writer, tx, inserts, logger } = setup(); + + await writer.write( + tx, + [block(100, [{ kind: "providerAttributesSigned", owner: OWNER, auditor: AUDITOR, attributes: [{ key: "region", value: "us-west" }] }])], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, ProviderAuditSignatures)).toHaveLength(1); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("locks provider state rows for update ordered by owner account id", async () => { + const { writer, tx, stateLocks } = setup(); + + await writer.write(tx, [block(100, [created()])], ACCOUNT_IDS); + + expect(stateLocks).toHaveLength(1); + expect(stateLocks[0].orderBy).toHaveLength(1); + expect(stateLocks[0].orderBy[0]).toBe(Providers.ownerAccountId); + expect(stateLocks[0].for).toEqual(["update"]); + }); + + it("serializes audit writes under a single transaction-scoped advisory lock", async () => { + const { writer, tx, executes } = setup(); + + await writer.write( + tx, + [ + block(100, [{ kind: "providerAttributesSigned", owner: OWNER, auditor: AUDITOR, attributes: [{ key: "region", value: "us-west" }] }]), + block(101, [{ kind: "providerAttributesUnsigned", owner: OWNER, auditor: AUDITOR, keys: ["region"] }]) + ], + ACCOUNT_IDS + ); + + expect(executes).toHaveLength(1); + expect(sqlText(executes[0])).toContain("pg_advisory_xact_lock"); + }); + + it("does not take the audit advisory lock when the batch has no audit changes", async () => { + const { writer, tx, executes } = setup(); + + await writer.write(tx, [block(100, [created()])], ACCOUNT_IDS); + + expect(executes).toEqual([]); + }); + + function setup(input?: { providers?: Record[] }) { + const inserts: { table: unknown; rows: Record[] }[] = []; + const upserts: { table: unknown; config: Record }[] = []; + const deletes: { table: unknown; where: unknown }[] = []; + const selects: unknown[] = []; + const executes: SQL[] = []; + const stateLocks: { orderBy: unknown[]; for: unknown[] }[] = []; + + const selectChain = () => { + const lock: { orderBy: unknown[]; for: unknown[] } = { orderBy: [], for: [] }; + stateLocks.push(lock); + const chain = { + where: () => chain, + orderBy: (...args: unknown[]) => { + lock.orderBy = args; + return chain; + }, + for: (...args: unknown[]) => { + lock.for = args; + return chain; + }, + then: (resolve: (rows: unknown[]) => unknown, reject?: (error: unknown) => unknown) => Promise.resolve(input?.providers ?? []).then(resolve, reject) + }; + return chain; + }; + + const tx = { + insert: (table: unknown) => ({ + values: (rows: Record | Record[]) => { + const rowArray = Array.isArray(rows) ? rows : [rows]; + inserts.push({ table, rows: rowArray }); + return Object.assign(Promise.resolve(), { + onConflictDoUpdate: (config: Record) => { + upserts.push({ table, config }); + return Promise.resolve(); + } + }); + } + }), + select: (fields?: unknown) => { + selects.push(fields); + return { from: () => selectChain() }; + }, + delete: (table: unknown) => ({ + where: (condition: unknown) => { + deletes.push({ table, where: condition }); + return Promise.resolve(); + } + }), + execute: (query: SQL) => { + executes.push(query); + return Promise.resolve(); + } + }; + + const logger = mock(); + return { writer: new ProviderWriter(logger), tx: tx as unknown as ChainTransaction, inserts, upserts, deletes, selects, executes, stateLocks, logger }; + } + + function providerRow(overrides: Record) { + return { + ownerAccountId: 7, + hostUri: "https://provider.example.com:8443", + email: null, + website: null, + attributes: [], + lastProcessedHeight: 100, + createdHeight: 100, + updatedHeight: null, + deletedHeight: null, + ...overrides + }; + } + + function created(overrides?: Partial>): AkashChangeBody { + return { + kind: "providerCreated", + owner: OWNER, + hostUri: "https://provider.example.com:8443", + email: null, + website: null, + attributes: [{ key: "region", value: "us-west" }], + ...overrides + }; + } + + function updated(overrides: Partial>): AkashChangeBody { + return { + kind: "providerUpdated", + owner: OWNER, + hostUri: "https://provider.example.com:8443", + email: null, + website: null, + attributes: [], + ...overrides + }; + } + + function block(height: number, bodies: AkashChangeBody[]): AkashBlockChanges { + return { height, datetime: BLOCK_TIME, changes: bodies.map((body, index) => ({ ...body, txIndex: 0, msgIndex: index })) }; + } + + function rowsFor(inserts: { table: unknown; rows: Record[] }[], table: unknown): Record[] { + return inserts.filter(insert => insert.table === table).flatMap(insert => insert.rows); + } + + function sqlText(query: SQL): string { + return new PgDialect().sqlToQuery(query).sql; + } +}); diff --git a/apps/chain-indexer/src/akash/provider-writer.service.ts b/apps/chain-indexer/src/akash/provider-writer.service.ts new file mode 100644 index 0000000000..f38198f0e3 --- /dev/null +++ b/apps/chain-indexer/src/akash/provider-writer.service.ts @@ -0,0 +1,280 @@ +import { and, eq, inArray, lte, sql } from "drizzle-orm"; +import chunk from "lodash/chunk"; +import { inject, singleton } from "tsyringe"; + +import type { AkashBlockChanges, ProviderAttribute, ProviderChange } from "@src/akash/akash-changes"; +import { isProviderAuditChange, isProviderChange, isProviderRegistryChange } from "@src/akash/akash-changes"; +import { INSERT_CHUNK_SIZE } from "@src/db/insert-chunk-size"; +import { ProviderAuditSignatures, Providers } from "@src/db/schema"; +import { sqlExcluded } from "@src/db/sql-excluded"; +import type { ChainTransaction } from "@src/providers/db.provider"; +import { LoggerService } from "@src/providers/logging.provider"; + +/** + * Serializes the audit-signature section across concurrent committers (overlapping sync pods during a + * rolling deploy, or sync racing a backfill). Writers apply signs and deletes in block order, so two + * writers with different block windows can lock the same (owner, auditor, key) rows in opposite orders + * and deadlock; the provider path avoids this with an ordered `FOR UPDATE`, but audit's delete-all-keys + * touches rows that cannot be pre-locked by key. One transaction-scoped advisory lock removes the + * hazard, and sparse audit traffic makes it near-free in steady state. Shares Postgres's advisory-lock + * namespace with MIGRATION_LOCK_KEY (db.provider.ts), so the value must stay distinct from it. + */ +const AUDIT_SIGNATURE_LOCK_KEY = 7_431_001; + +interface ProviderBlockChanges { + height: number; + changes: ProviderChange[]; +} + +interface ProviderState { + ownerAccountId: number; + hostUri: string; + email: string | null; + website: string | null; + attributes: ProviderAttribute[]; + lastProcessedHeight: number; + createdHeight: number; + updatedHeight: number | null; + deletedHeight: number | null; + touched: boolean; +} + +interface ProviderWarning { + code: "PROVIDER_ORPHAN_REFERENCE"; + kind: ProviderChange["kind"]; + owner: string; + height: number; +} + +/** + * Persists the provider registry and audited attributes inside the block transaction, mirroring the + * fold-then-flush shape of AkashWriter for the aggregate keyed by owner instead of (owner, dseq). + * Provider rows are locked FOR UPDATE in ascending owner-account order and flushed with the + * `last_processed_height` watermark guard, so overlapping writers replaying the same blocks stay + * idempotent. Audit signatures skip the fold: each sign/unsign applies in block order with a per-row + * height guard, serialized by a transaction-scoped advisory lock so overlapping writers can't lock + * audit rows in conflicting orders. x/audit state is independent of x/provider and audit traffic is + * sparse, so folding it is unnecessary and the lock is near-free. + */ +@singleton() +export class ProviderWriter { + readonly #logger: LoggerService; + + constructor(@inject(LoggerService) logger: LoggerService) { + this.#logger = logger; + this.#logger.setContext("PROVIDER_WRITER"); + } + + async write(tx: ChainTransaction, blocks: AkashBlockChanges[], accountIds: Map): Promise { + const withChanges = blocks + .map(block => ({ height: block.height, changes: block.changes.filter(isProviderChange) })) + .filter(block => block.changes.length > 0); + if (withChanges.length === 0) { + return; + } + + await this.#writeProviders(tx, withChanges, accountIds); + await this.#writeAuditSignatures(tx, withChanges, accountIds); + } + + async #writeProviders(tx: ChainTransaction, blocks: ProviderBlockChanges[], accountIds: Map): Promise { + const ownerIds = this.#collectOwnerIds(blocks, accountIds); + if (ownerIds.size === 0) { + return; + } + + const states = await this.#loadStates( + tx, + [...ownerIds.values()].sort((a, b) => a - b) + ); + const warnings: ProviderWarning[] = []; + + for (const block of blocks) { + this.#applyBlockChanges(states, block, ownerIds, warnings); + } + this.#logWarnings(warnings); + + const touched = [...states.values()].filter(state => state.touched); + if (touched.length === 0) { + return; + } + + await tx + .insert(Providers) + .values(touched.map(({ touched: _, ...row }) => row)) + .onConflictDoUpdate({ + target: Providers.ownerAccountId, + set: { + hostUri: sqlExcluded("host_uri"), + email: sqlExcluded("email"), + website: sqlExcluded("website"), + attributes: sqlExcluded("attributes"), + lastProcessedHeight: sqlExcluded("last_processed_height"), + createdHeight: sqlExcluded("created_height"), + updatedHeight: sqlExcluded("updated_height"), + deletedHeight: sqlExcluded("deleted_height") + }, + setWhere: sql`excluded.last_processed_height >= ${Providers.lastProcessedHeight}` + }); + } + + #applyBlockChanges(states: Map, block: ProviderBlockChanges, ownerIds: Map, warnings: ProviderWarning[]): void { + const skippedOwners = this.#ownersAtOrPastWatermark(states, block.height); + + for (const change of block.changes) { + if (isProviderAuditChange(change)) { + continue; + } + const ownerAccountId = this.#requireId(ownerIds, change.owner); + if (skippedOwners.has(ownerAccountId)) { + continue; + } + + const state = states.get(ownerAccountId); + if (change.kind === "providerCreated") { + states.set(ownerAccountId, { + ownerAccountId, + hostUri: change.hostUri, + email: change.email, + website: change.website, + attributes: change.attributes, + lastProcessedHeight: block.height, + createdHeight: block.height, + updatedHeight: null, + deletedHeight: null, + touched: true + }); + continue; + } + + if (!state) { + warnings.push({ code: "PROVIDER_ORPHAN_REFERENCE", kind: change.kind, owner: change.owner, height: block.height }); + continue; + } + + if (change.kind === "providerUpdated") { + state.hostUri = change.hostUri; + state.email = change.email; + state.website = change.website; + state.attributes = change.attributes; + state.updatedHeight = block.height; + } else { + state.deletedHeight = block.height; + } + state.lastProcessedHeight = block.height; + state.touched = true; + } + } + + /** Providers already at or past this block's height saw it in a previous commit; the whole block is a duplicate for them. */ + #ownersAtOrPastWatermark(states: Map, height: number): Set { + const skipped = new Set(); + for (const state of states.values()) { + if (state.lastProcessedHeight >= height) { + skipped.add(state.ownerAccountId); + } + } + return skipped; + } + + async #writeAuditSignatures(tx: ChainTransaction, blocks: ProviderBlockChanges[], accountIds: Map): Promise { + if (!blocks.some(block => block.changes.some(isProviderAuditChange))) { + return; + } + await tx.execute(sql`SELECT pg_advisory_xact_lock(${AUDIT_SIGNATURE_LOCK_KEY})`); + + for (const block of blocks) { + for (const change of block.changes) { + if (change.kind === "providerAttributesSigned") { + await this.#upsertSignatures(tx, change, block.height, accountIds); + } else if (change.kind === "providerAttributesUnsigned") { + await this.#deleteSignatures(tx, change, block.height, accountIds); + } + } + } + } + + async #upsertSignatures( + tx: ChainTransaction, + change: Extract, + height: number, + accountIds: Map + ): Promise { + const ownerAccountId = this.#requireId(accountIds, change.owner); + const auditorAccountId = this.#requireId(accountIds, change.auditor); + const rows = dedupeByKeyLastWins(change.attributes).map(attribute => ({ + ownerAccountId, + auditorAccountId, + key: attribute.key, + value: attribute.value, + height + })); + + for (const rowChunk of chunk(rows, INSERT_CHUNK_SIZE)) { + await tx + .insert(ProviderAuditSignatures) + .values(rowChunk) + .onConflictDoUpdate({ + target: [ProviderAuditSignatures.ownerAccountId, ProviderAuditSignatures.auditorAccountId, ProviderAuditSignatures.key], + set: { value: sqlExcluded("value"), height: sqlExcluded("height") }, + setWhere: sql`excluded.height >= ${ProviderAuditSignatures.height}` + }); + } + } + + /** The `height <=` guard keeps a replayed delete from removing a signature re-signed at a later height. */ + async #deleteSignatures( + tx: ChainTransaction, + change: Extract, + height: number, + accountIds: Map + ): Promise { + const identity = and( + eq(ProviderAuditSignatures.ownerAccountId, this.#requireId(accountIds, change.owner)), + eq(ProviderAuditSignatures.auditorAccountId, this.#requireId(accountIds, change.auditor)), + lte(ProviderAuditSignatures.height, height) + ); + await tx.delete(ProviderAuditSignatures).where(change.keys.length > 0 ? and(identity, inArray(ProviderAuditSignatures.key, change.keys)) : identity); + } + + #collectOwnerIds(blocks: ProviderBlockChanges[], accountIds: Map): Map { + const ownerIds = new Map(); + for (const block of blocks) { + for (const change of block.changes) { + if (isProviderRegistryChange(change)) { + ownerIds.set(change.owner, this.#requireId(accountIds, change.owner)); + } + } + } + return ownerIds; + } + + async #loadStates(tx: ChainTransaction, ownerAccountIds: number[]): Promise> { + const rows = await tx.select().from(Providers).where(inArray(Providers.ownerAccountId, ownerAccountIds)).orderBy(Providers.ownerAccountId).for("update"); + + return new Map(rows.map(row => [row.ownerAccountId, { ...row, touched: false }])); + } + + #logWarnings(warnings: ProviderWarning[]): void { + if (warnings.length === 0) { + return; + } + this.#logger.warn({ event: "PROVIDER_ORPHAN_REFERENCE", count: warnings.length, samples: warnings.slice(0, 5) }); + } + + #requireId(accountIds: Map, address: string): number { + const id = accountIds.get(address); + if (id === undefined) { + throw new Error(`No interned account id for address ${address}`); + } + return id; + } +} + +/** + * A message may sign the same key twice; feeding both rows to one ON CONFLICT DO UPDATE would raise + * `21000: command cannot affect row a second time`, so collapse to the last occurrence. + */ +function dedupeByKeyLastWins(attributes: ProviderAttribute[]): ProviderAttribute[] { + return [...new Map(attributes.map(attribute => [attribute.key, attribute])).values()]; +} diff --git a/apps/chain-indexer/src/akash/resources.ts b/apps/chain-indexer/src/akash/resources.ts index b9f813c882..f56954a3cf 100644 --- a/apps/chain-indexer/src/akash/resources.ts +++ b/apps/chain-indexer/src/akash/resources.ts @@ -1,4 +1,4 @@ -import type { NormalizedGroup, NormalizedResource } from "@src/akash/akash-changes"; +import type { NormalizedGroup, NormalizedResource, ProviderAttribute } from "@src/akash/akash-changes"; import { asRecord } from "@src/akash/json"; /** @@ -75,7 +75,7 @@ function gpuAttributes(gpu: Record | null): { vendor: string | return { vendor: match[1], model: match[2] !== "*" ? match[2] : null }; } -function attributeList(attributes: unknown): Array<{ key: string; value: string }> { +export function attributeList(attributes: unknown): ProviderAttribute[] { if (!Array.isArray(attributes)) { return []; } diff --git a/apps/chain-indexer/src/db/schema.spec.ts b/apps/chain-indexer/src/db/schema.spec.ts index ef9abcd1a9..3ddc63b16e 100644 --- a/apps/chain-indexer/src/db/schema.spec.ts +++ b/apps/chain-indexer/src/db/schema.spec.ts @@ -17,6 +17,8 @@ import { ProposalDeposits, Proposals, ProposalVotes, + ProviderAuditSignatures, + Providers, UnbondingDelegations, Validators } from "@src/db/schema"; @@ -184,3 +186,25 @@ describe("akash deployment schema", () => { expect(config.columns.find(column => column.name === "tx_index")?.notNull).toBe(false); }); }); + +describe("akash provider schema", () => { + it("keys providers by their owner account with lifecycle heights and the replay watermark", () => { + const config = getTableConfig(Providers); + + expect(config.name).toBe("providers"); + expect(config.columns.find(column => column.name === "owner_account_id")?.primary).toBe(true); + expect(config.foreignKeys[0].reference().foreignColumns[0].name).toBe("id"); + expect(config.columns.map(column => column.name)).toEqual( + expect.arrayContaining(["host_uri", "email", "website", "attributes", "last_processed_height", "created_height", "updated_height", "deleted_height"]) + ); + }); + + it("keys audit signatures by owner, auditor and key with account foreign keys for both parties", () => { + const config = getTableConfig(ProviderAuditSignatures); + + expect(config.primaryKeys[0].columns.map(column => column.name)).toEqual(["owner_account_id", "auditor_account_id", "key"]); + expect(config.foreignKeys).toHaveLength(2); + config.foreignKeys.forEach(foreignKey => expect(foreignKey.reference().foreignColumns[0].name).toBe("id")); + expect(config.columns.map(column => column.name)).toEqual(expect.arrayContaining(["value", "height"])); + }); +}); diff --git a/apps/chain-indexer/src/db/schema.ts b/apps/chain-indexer/src/db/schema.ts index 2290be387f..99fcb2dd0b 100644 --- a/apps/chain-indexer/src/db/schema.ts +++ b/apps/chain-indexer/src/db/schema.ts @@ -16,6 +16,7 @@ import { uniqueIndex } from "drizzle-orm/pg-core"; +import type { ProviderAttribute } from "@src/akash/akash-changes"; import { bytea } from "@src/db/bytea"; export const cosmosSchema = pgSchema("cosmos"); @@ -505,3 +506,48 @@ export const DeploymentEvents = akashSchema.table( }, t => [primaryKey({ columns: [t.deploymentId, t.height, t.ordinal] })] ); + +/** + * Current on-chain provider state, one row per owner — mirroring `akash query provider list`. + * A provider that deletes and re-registers reuses its row: create resets `created_height` and + * clears `updated_height`/`deleted_height`, so the original registration height is not kept. + * Attributes are the full replace-on-update set from MsgCreate/MsgUpdateProvider. + * `last_processed_height` is the replay watermark (same semantics as deployments). + */ +export const Providers = akashSchema.table("providers", { + ownerAccountId: integer("owner_account_id") + .primaryKey() + .references(() => Accounts.id), + hostUri: text("host_uri").notNull(), + email: text("email"), + website: text("website"), + attributes: jsonb("attributes").$type().notNull(), + lastProcessedHeight: bigint("last_processed_height", { mode: "number" }).notNull(), + createdHeight: bigint("created_height", { mode: "number" }).notNull(), + updatedHeight: bigint("updated_height", { mode: "number" }), + deletedHeight: bigint("deleted_height", { mode: "number" }) +}); + +/** + * Audited provider attributes, one row per (owner, auditor, key) — mirroring the x/audit store. + * Keyed by account rather than the providers row because x/audit never consults x/provider: + * signatures survive provider deletion and can precede registration. MsgSignProviderAttributes + * merges per-key; MsgDeleteProviderAttributes deletes the given keys, or all of the auditor's + * keys when none are given. `height` is the per-row replay guard: signs only apply at or above + * it and deletes only remove rows written at or below the deleting block. + */ +export const ProviderAuditSignatures = akashSchema.table( + "provider_audit_signatures", + { + ownerAccountId: integer("owner_account_id") + .notNull() + .references(() => Accounts.id), + auditorAccountId: integer("auditor_account_id") + .notNull() + .references(() => Accounts.id), + key: text("key").notNull(), + value: text("value").notNull(), + height: bigint("height", { mode: "number" }).notNull() + }, + t => [primaryKey({ columns: [t.ownerAccountId, t.auditorAccountId, t.key] })] +); diff --git a/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts b/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts index b1e32e3c35..eb276ac896 100644 --- a/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts +++ b/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; import { mock } from "vitest-mock-extended"; import type { AkashWriter } from "@src/akash/akash-writer.service"; +import type { ProviderWriter } from "@src/akash/provider-writer.service"; import { AccountTxs, Blocks, IndexerState, MessageDeadLetters, Messages, MessageTypes } from "@src/db/schema"; import type { GovWriter } from "@src/gov/gov-writer.service"; import type { AccountInterner } from "@src/pipeline/balance/account-interner.service"; @@ -255,6 +256,18 @@ describe(BlockCommitterService.name, () => { const accountIds = akashWriter.write.mock.calls[0][2]; expect(accountIds.get("akash1owner")).toBeDefined(); }); + + it("hands the provider writer the same derived changes and account ids as the akash writer", async () => { + const { committer, akashWriter, providerWriter } = setup({ selectResults: [[{ id: 7, type: MSG_SEND }]] }); + + await committer.commit( + buildBlock([MSG_SEND], 10, { + events: [{ type: "akash.deployment.v1.EventDeploymentClosed", attributes: { id: '{"owner":"akash1owner","dseq":"42"}' } }] + }) + ); + + expect(providerWriter.write).toHaveBeenCalledWith(expect.anything(), akashWriter.write.mock.calls[0][1], akashWriter.write.mock.calls[0][2]); + }); }); function setup(input?: { @@ -307,9 +320,10 @@ describe(BlockCommitterService.name, () => { const govWriter = mock(); const akashWriter = mock(); + const providerWriter = mock(); const logger = mock(); - const committer = new BlockCommitterService(dbFake as unknown as ChainDatabase, interner, balanceWriter, govWriter, akashWriter, logger); - return { committer, insertedRows, conflictUpdates, deletions, interner, balanceWriter, govWriter, akashWriter, logger }; + const committer = new BlockCommitterService(dbFake as unknown as ChainDatabase, interner, balanceWriter, govWriter, akashWriter, providerWriter, logger); + return { committer, insertedRows, conflictUpdates, deletions, interner, balanceWriter, govWriter, akashWriter, providerWriter, logger }; } function buildBlock( diff --git a/apps/chain-indexer/src/pipeline/block-committer.service.ts b/apps/chain-indexer/src/pipeline/block-committer.service.ts index d9568d28a8..a1aec9c2b9 100644 --- a/apps/chain-indexer/src/pipeline/block-committer.service.ts +++ b/apps/chain-indexer/src/pipeline/block-committer.service.ts @@ -6,6 +6,7 @@ import type { AkashBlockChanges } from "@src/akash/akash-changes"; import { collectAkashAddresses } from "@src/akash/akash-changes"; import { deriveAkashChanges } from "@src/akash/akash-deriver"; import { AkashWriter } from "@src/akash/akash-writer.service"; +import { ProviderWriter } from "@src/akash/provider-writer.service"; import { INSERT_CHUNK_SIZE } from "@src/db/insert-chunk-size"; import { insertChunked } from "@src/db/insert-chunked"; import { AccountTxs, Blocks, IndexerState, MessageDeadLetters, Messages, MessageTypes, Transactions } from "@src/db/schema"; @@ -47,6 +48,7 @@ export class BlockCommitterService { readonly #balanceWriter: BalanceWriter; readonly #govWriter: GovWriter; readonly #akashWriter: AkashWriter; + readonly #providerWriter: ProviderWriter; readonly #logger: LoggerService; readonly #moduleRegistry = buildModuleAddressRegistry(); readonly #typeIds = new Map(); @@ -57,6 +59,7 @@ export class BlockCommitterService { @inject(BalanceWriter) balanceWriter: BalanceWriter, @inject(GovWriter) govWriter: GovWriter, @inject(AkashWriter) akashWriter: AkashWriter, + @inject(ProviderWriter) providerWriter: ProviderWriter, @inject(LoggerService) logger: LoggerService ) { this.#db = db; @@ -64,6 +67,7 @@ export class BlockCommitterService { this.#balanceWriter = balanceWriter; this.#govWriter = govWriter; this.#akashWriter = akashWriter; + this.#providerWriter = providerWriter; this.#logger = logger; this.#logger.setContext("COMMITTER"); } @@ -148,6 +152,7 @@ export class BlockCommitterService { await insertChunked(tx, AccountTxs, accountTxRows); await this.#govWriter.writeForBlocks(tx, blocks, accountIds); await this.#akashWriter.write(tx, akashChanges, accountIds); + await this.#providerWriter.write(tx, akashChanges, accountIds); await tx .insert(IndexerState)