diff --git a/server/api/[version]/validators/index.get.ts b/server/api/[version]/validators/index.get.ts index 6362ae0..b4fe77a 100644 --- a/server/api/[version]/validators/index.get.ts +++ b/server/api/[version]/validators/index.get.ts @@ -4,7 +4,7 @@ import { mainQuerySchema } from '~~/server/utils/schemas' import { fetchValidators } from '~~/server/utils/validators' export default defineEventHandler(async (event) => { - const { payoutType, onlyActive, onlyKnown } = await getValidatedQuery(event, mainQuerySchema.parse) + const { payoutType, onlyActive, onlyKnown, withIdenticon } = await getValidatedQuery(event, mainQuerySchema.parse) let addresses: string[] = [] let activeValidators: Validator[] = [] @@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => { addresses = activeValidators.map(v => v.address) } - const { data: validators, error: errorValidators } = await fetchValidators({ payoutType, addresses, onlyKnown }) + const { data: validators, error: errorValidators } = await fetchValidators({ payoutType, addresses, onlyKnown, withIdenticon }) if (errorValidators || !validators) throw createError(errorValidators) diff --git a/server/database/migrations/0001_flashy_menace.sql b/server/database/migrations/0001_flashy_menace.sql new file mode 100644 index 0000000..93c085d --- /dev/null +++ b/server/database/migrations/0001_flashy_menace.sql @@ -0,0 +1,23 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_validators` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text DEFAULT 'Unknown validator' NOT NULL, + `address` text NOT NULL, + `description` text, + `fee` real DEFAULT -1, + `payout_type` text DEFAULT 'none', + `payout_schedule` text DEFAULT '', + `is_maintained_by_nimiq` integer DEFAULT false, + `icon` text NOT NULL, + `has_default_icon` integer DEFAULT true NOT NULL, + `accent_color` text NOT NULL, + `website` text, + `contact` text, + CONSTRAINT "enum_check" CHECK("__new_validators"."payout_type" IN ('none', 'restake', 'direct')) +); +--> statement-breakpoint +INSERT INTO `__new_validators`("id", "name", "address", "description", "fee", "payout_type", "payout_schedule", "is_maintained_by_nimiq", "icon", "has_default_icon", "accent_color", "website", "contact") SELECT "id", "name", "address", "description", "fee", "payout_type", "payout_schedule", "is_maintained_by_nimiq", "icon", "has_default_icon", "accent_color", "website", "contact" FROM `validators`;--> statement-breakpoint +DROP TABLE `validators`;--> statement-breakpoint +ALTER TABLE `__new_validators` RENAME TO `validators`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `validators_address_unique` ON `validators` (`address`); \ No newline at end of file diff --git a/server/database/migrations/0001_sloppy_slayback.sql b/server/database/migrations/0001_sloppy_slayback.sql new file mode 100644 index 0000000..bd5e2a2 --- /dev/null +++ b/server/database/migrations/0001_sloppy_slayback.sql @@ -0,0 +1 @@ +ALTER TABLE `validators` ADD `has_default_icon` integer NOT NULL; \ No newline at end of file diff --git a/server/database/migrations/meta/0001_snapshot.json b/server/database/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3a13794 --- /dev/null +++ b/server/database/migrations/meta/0001_snapshot.json @@ -0,0 +1,324 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "fae09d88-f7e3-445e-8852-da3b26ddead3", + "prevId": "95729871-3086-4fde-a2c8-7666bec7aef4", + "tables": { + "activity": { + "name": "activity", + "columns": { + "validator_id": { + "name": "validator_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "epoch_number": { + "name": "epoch_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "likelihood": { + "name": "likelihood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rewarded": { + "name": "rewarded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "missed": { + "name": "missed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_ratio": { + "name": "size_ratio", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_ratio_via_slots": { + "name": "size_ratio_via_slots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_election_block": { + "name": "idx_election_block", + "columns": [ + "epoch_number" + ], + "isUnique": false + } + }, + "foreignKeys": { + "activity_validator_id_validators_id_fk": { + "name": "activity_validator_id_validators_id_fk", + "tableFrom": "activity", + "tableTo": "validators", + "columnsFrom": [ + "validator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "activity_validator_id_epoch_number_pk": { + "columns": [ + "validator_id", + "epoch_number" + ], + "name": "activity_validator_id_epoch_number_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scores": { + "name": "scores", + "columns": { + "validator_id": { + "name": "validator_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from_epoch": { + "name": "from_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to_epoch": { + "name": "to_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total": { + "name": "total", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "liveness": { + "name": "liveness", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reliability": { + "name": "reliability", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_validator_id": { + "name": "idx_validator_id", + "columns": [ + "validator_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "scores_validator_id_validators_id_fk": { + "name": "scores_validator_id_validators_id_fk", + "tableFrom": "scores", + "tableTo": "validators", + "columnsFrom": [ + "validator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scores_validator_id_from_epoch_to_epoch_pk": { + "columns": [ + "validator_id", + "from_epoch", + "to_epoch" + ], + "name": "scores_validator_id_from_epoch_to_epoch_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "validators": { + "name": "validators", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Unknown validator'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fee": { + "name": "fee", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": -1 + }, + "payout_type": { + "name": "payout_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'none'" + }, + "payout_schedule": { + "name": "payout_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "is_maintained_by_nimiq": { + "name": "is_maintained_by_nimiq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_default_icon": { + "name": "has_default_icon", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "validators_address_unique": { + "name": "validators_address_unique", + "columns": [ + "address" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "enum_check": { + "name": "enum_check", + "value": "\"validators\".\"payout_type\" IN ('none', 'restake', 'direct')" + } + } + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json index 50ead20..ac44e82 100644 --- a/server/database/migrations/meta/_journal.json +++ b/server/database/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1731440861436, "tag": "0000_cultured_the_fury", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1731773451802, + "tag": "0001_flashy_menace", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/database/schema.ts b/server/database/schema.ts index e7e10a9..fcc25b8 100644 --- a/server/database/schema.ts +++ b/server/database/schema.ts @@ -12,6 +12,7 @@ export const validators = sqliteTable('validators', { payoutSchedule: text('payout_schedule').default(''), isMaintainedByNimiq: integer('is_maintained_by_nimiq', { mode: 'boolean' }).default(false), icon: text('icon').notNull(), + hasDefaultIcon: integer('has_default_icon', { mode: 'boolean' }).notNull().default(true), accentColor: text('accent_color').notNull(), website: text('website'), contact: text('contact', { mode: 'json' }), diff --git a/server/utils/schemas.ts b/server/utils/schemas.ts index d292279..a6c60e1 100644 --- a/server/utils/schemas.ts +++ b/server/utils/schemas.ts @@ -22,6 +22,7 @@ export const validatorSchema = z.object({ description: z.string().optional(), website: z.string().url().optional(), icon: z.string().optional(), + hasDefaultIcon: z.boolean(), accentColor: z.string().optional(), contact: z.object({ email: z.string().email().optional(), @@ -41,4 +42,5 @@ export const mainQuerySchema = z.object({ payoutType: z.nativeEnum(PayoutType).optional(), onlyActive: z.literal('true').or(z.literal('false')).default('false').transform(v => v === 'true'), onlyKnown: z.literal('true').or(z.literal('false')).default('false').transform(v => v === 'true'), + withIdenticon: z.literal('true').or(z.literal('false')).default('false').transform(v => v === 'true'), }) diff --git a/server/utils/validators.ts b/server/utils/validators.ts index 98d49b6..4b44f47 100644 --- a/server/utils/validators.ts +++ b/server/utils/validators.ts @@ -36,7 +36,7 @@ interface StoreValidatorOptions { export async function storeValidator( address: string, - rest: Omit & { icon?: string, accentColor?: string } = {}, + rest: Omit & { icon?: string, accentColor?: string } = {}, options: StoreValidatorOptions = {}, ): Promise { try { @@ -76,11 +76,11 @@ export async function storeValidator( // TODO Once the validators have accent colors, re-enable this check // if (!rest.accentColor) // throw new Error(`The validator ${address} does have an icon but not an accent color`) - return { icon: rest.icon, accentColor: rest.accentColor! } + return { icon: rest.icon, accentColor: rest.accentColor!, hasDefaultIcon: false } } const icon = await createIdenticon(address, { format: 'image/svg+xml' }) const { colors: { background: accentColor } } = await getIdenticonsParams(address) - return { icon, accentColor } + return { icon, accentColor, hasDefaultIcon: true } } const brandingParameters = await getBrandingParameters() @@ -134,9 +134,10 @@ export interface FetchValidatorsOptions { payoutType?: PayoutType addresses?: string[] onlyKnown?: boolean + withIdenticon?: boolean } -export async function fetchValidators({ payoutType, addresses = [], onlyKnown = false }: FetchValidatorsOptions): Result { +export async function fetchValidators({ payoutType, addresses = [], onlyKnown = false, withIdenticon = false }: FetchValidatorsOptions): Result { const filters = [] if (payoutType) filters.push(eq(tables.validators.payoutType, payoutType)) @@ -144,6 +145,8 @@ export async function fetchValidators({ payoutType, addresses = [], onlyKnown = filters.push(inArray(tables.validators.address, addresses)) if (onlyKnown) filters.push(not(eq(tables.validators.name, 'Unknown validator'))) + if (!withIdenticon) + filters.push(eq(tables.validators.hasDefaultIcon, true)) const validators = await useDrizzle() .select()