From c411e370003345185623905d7b0d345e3529a08c Mon Sep 17 00:00:00 2001 From: Neo Sun Date: Thu, 31 Jul 2025 20:14:01 +1200 Subject: [PATCH 1/3] feat(validators): add validators query in graphql --- schema.graphql | 50 ++++++++++++++++++++++++++ src/bin/mock-data.rs | 80 ++++++++++++++++++++++++++++++++++++++++- src/models/header.rs | 10 ++++++ src/models/validator.rs | 63 +++++++++++++++++++++++++++++++- src/schema/root.rs | 27 ++++++++++++++ 5 files changed, 228 insertions(+), 2 deletions(-) diff --git a/schema.graphql b/schema.graphql index c158cd7..9361c4d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -359,6 +359,15 @@ type QueryRoot { """ epoch(id: Int!): Epoch """ + List all validators + """ + validators( first: Int = 10, + """ + Cursor for pagination + """ + after: String + ): ValidatorConnection! + """ Get the epoch by id/ed25519 """ validator(id: Int, ed25519: String): Validator @@ -547,6 +556,47 @@ type Validator { """ after: String ): HeaderConnection! + """ + Count the total blocks number + """ + totalBlocks: Int! + """ + Count the total tickets number + """ + totalTickets: Int! + """ + Count the total epochs number + """ + totalEpochs: Int! +} + +type ValidatorConnection { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + """ + A list of edges. + """ + edges: [ValidatorEdge!]! + """ + A list of nodes. + """ + nodes: [Validator!]! +} + +""" +An edge in a connection. +""" +type ValidatorEdge { + """ + The item at the end of the edge + """ + node: Validator! + """ + A cursor for use in pagination + """ + cursor: String! } type WorkResult { diff --git a/src/bin/mock-data.rs b/src/bin/mock-data.rs index d9fff9f..7b47064 100644 --- a/src/bin/mock-data.rs +++ b/src/bin/mock-data.rs @@ -38,5 +38,83 @@ async fn main() { } println!("Generated mock services"); - // mock work report + // mock epoch for next generate + for epoch in 10000..10020i32 { + sqlx::query!( + "INSERT INTO epochs (id, block,entropy,tickets_entropy) VALUES ($1,$2,$3,$4)", + epoch, + epoch, + "0xthisismockedepoch0000000000000000000000000000000000000000000info", + "0xthisismockedepoch00000000000000000000000000000000000000000ticket", + ) + .execute(&pool) + .await + .unwrap(); + } + + // mock core history (vindex (8, 9), with epochs (10000 - 10020)) + for vindex in 8..10i32 { + for epoch in 10000..10020i32 { + let v1 = Sha256::digest(epoch.to_be_bytes())[0]; + let v2 = (v1 as f64 / 255.0) * (1.25 - 0.75) + 0.75; // (0.75; ~ 1.25) + let extrinsic_count = (epoch as f64 * v2) as i32; + sqlx::query!( + "INSERT INTO epochs_cores (epoch_id,vindex,gas_used,imports,extrinsic_count,extrinsic_size,exports,bundle_size,da_load,popularity) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)", + epoch, + vindex, + epoch as i64 * 10, // gas_used + epoch / 100, // imports + extrinsic_count, + extrinsic_count * 100, // extrinsic size + extrinsic_count / 100, // exports + (epoch as f32 * 1.1) as i32, // bundle_size + epoch as i64 * 1000, // da_load + epoch as i64 / 100 // popularity + ).execute(&pool).await.unwrap(); + } + } + + // mock validator history (validators(11-20), with epochs (10000 - 10020)) + for vindex in 11..21i32 { + let r = Sha256::digest(vindex.to_be_bytes()); + let ed25519 = format!("0x{}", hex::encode(r)); + let bandersnatch = format!( + "0x{}", + hex::encode(Sha256::digest((vindex * 10).to_be_bytes())) + ); + + let validator: i32 = sqlx::query_scalar!( + "INSERT INTO validators (ed25519,bandersnatch,name,details,software,ip,website,scores) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + ed25519, + bandersnatch, + format!("Spacejam-{}", vindex), + "This is spacejam official node", + format!("spacejam v{}", r[0] % 3), + format!("{}.{}.{}.{}", r[0], r[1], r[2], r[3]), // ip + "https://spacejam.app", + vindex, // scores + ) + .fetch_one(&pool) + .await.unwrap(); + + for epoch in 10000..10020i32 { + let vindex = vindex - 10; + let v1 = Sha256::digest(vindex.to_be_bytes())[0]; + let blocks = ((v1 as f32 / 255.0) * 100f32) as i32; // (1 ~ 100) + + sqlx::query!( + "INSERT INTO epochs_validators (epoch_id,validator_id,vindex,blocks,tickets,preimages,guarantees,assurances) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)", + epoch, + validator, + vindex, + blocks, // blocks + blocks * 10, // tickets + blocks / 10, // preimages + blocks * 100, // guarantees + blocks / 20 // assurances + ) + .execute(&pool) + .await.unwrap(); + } + } } diff --git a/src/models/header.rs b/src/models/header.rs index 0584725..a74b69f 100644 --- a/src/models/header.rs +++ b/src/models/header.rs @@ -70,6 +70,16 @@ impl Header { Ok(data) } + /// count the validator's anchoring blocks + pub async fn count_by_author(pool: &PgPool, author: i32) -> Result { + let count = query_scalar!("SELECT COUNT(slot) FROM headers WHERE author_id=$1", author) + .fetch_one(pool) + .await? + .unwrap_or(0); + + Ok(count) + } + /// NOTE: this will not being used, consider remove it. pub async fn get(pool: &PgPool, slot: i32) -> Result { let data = query_as!(Self, "SELECT * FROM headers WHERE slot=$1", slot) diff --git a/src/models/validator.rs b/src/models/validator.rs index cecf428..1f50679 100644 --- a/src/models/validator.rs +++ b/src/models/validator.rs @@ -13,7 +13,7 @@ use sqlx::PgPool; #[derive(SimpleObject, Serialize, Deserialize)] #[graphql(complex)] pub struct Validator { - id: i32, + pub id: i32, ed25519: String, bandersnatch: String, name: String, @@ -73,9 +73,44 @@ impl Validator { .collect(); Ok(connection) } + + /// Count the total blocks number + pub async fn total_blocks(&self, ctx: &Context<'_>) -> GraphqlResult { + let pool = &ctx.data::()?.pg; + let count = Header::count_by_author(&pool, self.id).await?; + Ok(count) + } + + /// Count the total tickets number + pub async fn total_tickets(&self, ctx: &Context<'_>) -> GraphqlResult { + let pool = &ctx.data::()?.pg; + let count = EpochValidator::count_tickets_by_validator(&pool, self.id).await?; + Ok(count) + } + + /// Count the total epochs number + pub async fn total_epochs(&self, ctx: &Context<'_>) -> GraphqlResult { + let pool = &ctx.data::()?.pg; + let count = EpochValidator::count_epochs_by_validator(&pool, self.id).await?; + Ok(count) + } } impl Validator { + /// List all services (ASC) + pub async fn list(pool: &PgPool, limit: i32, cursor: i32) -> Result> { + let data = query_as!( + Self, + "SELECT * FROM validators WHERE id>$1 ORDER BY id ASC LIMIT $2", + cursor, + limit as i64 + 1 + ) + .fetch_all(pool) + .await?; + + Ok(data) + } + pub async fn get(pool: &PgPool, id: i32) -> Result { let data = query_as!(Self, "SELECT * FROM validators WHERE id = $1", id) .fetch_one(pool) @@ -193,6 +228,32 @@ impl EpochValidator { Ok(data) } + /// count the validator's epochs + pub async fn count_epochs_by_validator(pool: &PgPool, validator: i32) -> Result { + let count = query_scalar!( + "SELECT COUNT(id) FROM epochs_validators WHERE validator_id=$1", + validator + ) + .fetch_one(pool) + .await? + .unwrap_or(0); + + Ok(count) + } + + /// count the validator's tickets + pub async fn count_tickets_by_validator(pool: &PgPool, validator: i32) -> Result { + let count = query_scalar!( + "SELECT SUM(tickets) FROM epochs_validators WHERE validator_id=$1", + validator + ) + .fetch_one(pool) + .await? + .unwrap_or(0); + + Ok(count) + } + pub async fn insert(pool: &PgPool, epoch: i32, validator: i32, vindex: i32) -> Result<()> { if query_as!( Self, diff --git a/src/schema/root.rs b/src/schema/root.rs index 819f439..61d93ca 100644 --- a/src/schema/root.rs +++ b/src/schema/root.rs @@ -66,6 +66,33 @@ impl QueryRoot { Ok(epoch) } + /// List all validators + async fn validators( + &self, + ctx: &Context<'_>, + #[graphql(default = 10, validator(minimum = 1, maximum = 100))] first: Option, + #[graphql(desc = "Cursor for pagination")] after: Option, + ) -> Result> { + let limit = first.unwrap_or(10).min(100); + let cursor = after.unwrap_or_default().parse::().unwrap_or(0); + let pool = &ctx.data::()?.pg; + + let validators = Validator::list(pool, limit, cursor).await?; + let has_prev_page = cursor != 0; + let has_next_page = validators.len() > limit as usize; + let items = validators + .into_iter() + .take(limit as usize) + .collect::>(); + + let mut connection = Connection::new(has_prev_page, has_next_page); + connection.edges = items + .into_iter() + .map(|item| Edge::new(item.id.to_string(), item)) + .collect(); + Ok(connection) + } + /// Get the epoch by id/ed25519 async fn validator( &self, From 030889bed870cdaa25797579f07b6327d9700a2d Mon Sep 17 00:00:00 2001 From: Neo Sun Date: Thu, 31 Jul 2025 20:27:31 +1200 Subject: [PATCH 2/3] chore(sqlx): update sqlx prepare --- ...4633f2f107fa17e5ae0e30b13843517096f44.json | 29 ++++++++ ...08f74a642837935599ba8ed851e8ce5c1979b.json | 22 ++++++ ...65a1730e91b457b9cb08acb45509b7a717e21.json | 22 ++++++ ...faa164097b4c3a655f559f0b6e6b771c779f9.json | 71 +++++++++++++++++++ ...167f6c00914fe95e6c101fe74414077e3127e.json | 22 ++++++ 5 files changed, 166 insertions(+) create mode 100644 .sqlx/query-07a8ae7db2c978f20c0e29beaa34633f2f107fa17e5ae0e30b13843517096f44.json create mode 100644 .sqlx/query-1e48130eb6ea58976ffa55e0c1208f74a642837935599ba8ed851e8ce5c1979b.json create mode 100644 .sqlx/query-81a8dcb1f770b259155e18a163465a1730e91b457b9cb08acb45509b7a717e21.json create mode 100644 .sqlx/query-bc786b3f818e83eb420b5c3e326faa164097b4c3a655f559f0b6e6b771c779f9.json create mode 100644 .sqlx/query-c09ee53784c60d292109409084f167f6c00914fe95e6c101fe74414077e3127e.json diff --git a/.sqlx/query-07a8ae7db2c978f20c0e29beaa34633f2f107fa17e5ae0e30b13843517096f44.json b/.sqlx/query-07a8ae7db2c978f20c0e29beaa34633f2f107fa17e5ae0e30b13843517096f44.json new file mode 100644 index 0000000..7c6b285 --- /dev/null +++ b/.sqlx/query-07a8ae7db2c978f20c0e29beaa34633f2f107fa17e5ae0e30b13843517096f44.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO validators (ed25519,bandersnatch,name,details,software,ip,website,scores) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Text", + "Varchar", + "Varchar", + "Varchar", + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "07a8ae7db2c978f20c0e29beaa34633f2f107fa17e5ae0e30b13843517096f44" +} diff --git a/.sqlx/query-1e48130eb6ea58976ffa55e0c1208f74a642837935599ba8ed851e8ce5c1979b.json b/.sqlx/query-1e48130eb6ea58976ffa55e0c1208f74a642837935599ba8ed851e8ce5c1979b.json new file mode 100644 index 0000000..9e6f2a6 --- /dev/null +++ b/.sqlx/query-1e48130eb6ea58976ffa55e0c1208f74a642837935599ba8ed851e8ce5c1979b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(slot) FROM headers WHERE author_id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1e48130eb6ea58976ffa55e0c1208f74a642837935599ba8ed851e8ce5c1979b" +} diff --git a/.sqlx/query-81a8dcb1f770b259155e18a163465a1730e91b457b9cb08acb45509b7a717e21.json b/.sqlx/query-81a8dcb1f770b259155e18a163465a1730e91b457b9cb08acb45509b7a717e21.json new file mode 100644 index 0000000..4bd5313 --- /dev/null +++ b/.sqlx/query-81a8dcb1f770b259155e18a163465a1730e91b457b9cb08acb45509b7a717e21.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT SUM(tickets) FROM epochs_validators WHERE validator_id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sum", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "81a8dcb1f770b259155e18a163465a1730e91b457b9cb08acb45509b7a717e21" +} diff --git a/.sqlx/query-bc786b3f818e83eb420b5c3e326faa164097b4c3a655f559f0b6e6b771c779f9.json b/.sqlx/query-bc786b3f818e83eb420b5c3e326faa164097b4c3a655f559f0b6e6b771c779f9.json new file mode 100644 index 0000000..77150eb --- /dev/null +++ b/.sqlx/query-bc786b3f818e83eb420b5c3e326faa164097b4c3a655f559f0b6e6b771c779f9.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM validators WHERE id>$1 ORDER BY id ASC LIMIT $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "ed25519", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "bandersnatch", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "details", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "software", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "ip", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "website", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "scores", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "bc786b3f818e83eb420b5c3e326faa164097b4c3a655f559f0b6e6b771c779f9" +} diff --git a/.sqlx/query-c09ee53784c60d292109409084f167f6c00914fe95e6c101fe74414077e3127e.json b/.sqlx/query-c09ee53784c60d292109409084f167f6c00914fe95e6c101fe74414077e3127e.json new file mode 100644 index 0000000..03c9e34 --- /dev/null +++ b/.sqlx/query-c09ee53784c60d292109409084f167f6c00914fe95e6c101fe74414077e3127e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(id) FROM epochs_validators WHERE validator_id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "c09ee53784c60d292109409084f167f6c00914fe95e6c101fe74414077e3127e" +} From db2eb9983ad37daa6a48fd0ecc23725110550b17 Mon Sep 17 00:00:00 2001 From: Neo Sun Date: Thu, 31 Jul 2025 20:53:39 +1200 Subject: [PATCH 3/3] chore(clippy): fix clippy --- src/models/validator.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/validator.rs b/src/models/validator.rs index 1f50679..fce7447 100644 --- a/src/models/validator.rs +++ b/src/models/validator.rs @@ -77,21 +77,21 @@ impl Validator { /// Count the total blocks number pub async fn total_blocks(&self, ctx: &Context<'_>) -> GraphqlResult { let pool = &ctx.data::()?.pg; - let count = Header::count_by_author(&pool, self.id).await?; + let count = Header::count_by_author(pool, self.id).await?; Ok(count) } /// Count the total tickets number pub async fn total_tickets(&self, ctx: &Context<'_>) -> GraphqlResult { let pool = &ctx.data::()?.pg; - let count = EpochValidator::count_tickets_by_validator(&pool, self.id).await?; + let count = EpochValidator::count_tickets_by_validator(pool, self.id).await?; Ok(count) } /// Count the total epochs number pub async fn total_epochs(&self, ctx: &Context<'_>) -> GraphqlResult { let pool = &ctx.data::()?.pg; - let count = EpochValidator::count_epochs_by_validator(&pool, self.id).await?; + let count = EpochValidator::count_epochs_by_validator(pool, self.id).await?; Ok(count) } }