From 5e4b1e69425093f41923adf30e0975cda91f5dae Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 16 Jan 2025 20:20:19 +0200 Subject: [PATCH] app_store: add review protocol initial --- Cargo.lock | 83 ++++--- kinode/packages/app-store/Cargo.lock | 69 +++++- .../app-store/api/app-store:sys-v1.wit | 14 ++ .../app-store/app-store/src/http_api.rs | 22 ++ kinode/packages/app-store/chain/Cargo.toml | 2 +- kinode/packages/app-store/chain/src/lib.rs | 214 +++++++++++++++++- .../packages/app-store/ui/src/abis/helpers.ts | 4 +- .../packages/app-store/ui/src/abis/index.ts | 2 +- .../app-store/ui/src/pages/AppPage.tsx | 58 ++++- .../packages/app-store/ui/src/store/index.ts | 22 +- .../packages/app-store/ui/src/types/Apps.ts | 7 + 11 files changed, 439 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99c13f746..849f247d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,7 +94,7 @@ name = "alias" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -1275,7 +1275,7 @@ dependencies = [ "alloy-sol-types 0.8.15", "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand 0.8.5", "serde", @@ -1858,7 +1858,7 @@ name = "cat" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -1922,7 +1922,7 @@ dependencies = [ "alloy-sol-types 0.8.15", "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (git+https://github.com/kinode-dao/process_lib?rev=11630ce)", "process_macros", "rand 0.8.5", "serde", @@ -1941,7 +1941,7 @@ version = "0.2.1" dependencies = [ "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "pleco", "serde", "serde_json", @@ -2139,7 +2139,7 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" name = "contacts" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -2735,7 +2735,7 @@ name = "download" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -2747,7 +2747,7 @@ name = "downloads" version = "0.5.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand 0.8.5", "serde", @@ -2784,7 +2784,7 @@ dependencies = [ name = "echo" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "wit-bindgen 0.36.0", ] @@ -3052,7 +3052,7 @@ version = "0.2.0" dependencies = [ "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand 0.8.5", "serde", @@ -3206,7 +3206,7 @@ dependencies = [ name = "get_block" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -3402,7 +3402,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "help" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "wit-bindgen 0.36.0", ] @@ -3431,7 +3431,7 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" name = "hi" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -3464,7 +3464,7 @@ version = "0.1.2" dependencies = [ "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -3907,7 +3907,7 @@ name = "install" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -4085,7 +4085,7 @@ name = "kfetch" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rmp-serde", "serde", "serde_json", @@ -4097,7 +4097,7 @@ name = "kill" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -4211,6 +4211,29 @@ dependencies = [ "wit-bindgen 0.36.0", ] +[[package]] +name = "kinode_process_lib" +version = "0.10.1" +source = "git+https://github.com/kinode-dao/process_lib?rev=11630ce#11630cea54999548fec610ee41cd318cc6ab60b3" +dependencies = [ + "alloy 0.8.3", + "alloy-primitives 0.8.15", + "alloy-sol-macro 0.8.15", + "alloy-sol-types 0.8.15", + "anyhow", + "bincode", + "http 1.2.0", + "mime_guess", + "rand 0.8.5", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", + "wit-bindgen 0.36.0", +] + [[package]] name = "kit" version = "0.8.3" @@ -4257,7 +4280,7 @@ dependencies = [ "alloy-sol-types 0.8.15", "anyhow", "hex", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rmp-serde", "serde", @@ -4494,7 +4517,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex", "serde", "serde_json", @@ -4663,7 +4686,7 @@ dependencies = [ name = "net-diagnostics" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rmp-serde", "serde", "wit-bindgen 0.36.0", @@ -4706,7 +4729,7 @@ dependencies = [ name = "node_info" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -5033,7 +5056,7 @@ dependencies = [ name = "peer" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rmp-serde", "serde", "wit-bindgen 0.36.0", @@ -5043,7 +5066,7 @@ dependencies = [ name = "peers" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rmp-serde", "serde", "wit-bindgen 0.36.0", @@ -5700,7 +5723,7 @@ dependencies = [ name = "reset" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -6170,7 +6193,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rmp-serde", "serde", "serde_json", @@ -6391,7 +6414,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" name = "state" version = "0.1.0" dependencies = [ - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -6604,7 +6627,7 @@ version = "0.1.1" dependencies = [ "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.8.5", "regex", "serde", @@ -6618,7 +6641,7 @@ version = "0.1.1" dependencies = [ "anyhow", "bincode", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -6909,7 +6932,7 @@ version = "0.2.0" dependencies = [ "anyhow", "clap", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "wit-bindgen 0.36.0", @@ -7276,7 +7299,7 @@ name = "uninstall" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib 0.10.1", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", diff --git a/kinode/packages/app-store/Cargo.lock b/kinode/packages/app-store/Cargo.lock index 8d9ef0de7..0daa0c921 100644 --- a/kinode/packages/app-store/Cargo.lock +++ b/kinode/packages/app-store/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -556,7 +565,7 @@ dependencies = [ "alloy-sol-types", "anyhow", "bincode", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand", "serde", @@ -922,7 +931,7 @@ dependencies = [ "alloy-sol-types", "anyhow", "bincode", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (git+https://github.com/kinode-dao/process_lib?rev=11630ce)", "process_macros", "rand", "serde", @@ -1126,7 +1135,7 @@ name = "download" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -1138,7 +1147,7 @@ name = "downloads" version = "0.5.0" dependencies = [ "anyhow", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand", "serde", @@ -1314,7 +1323,7 @@ version = "0.2.0" dependencies = [ "anyhow", "bincode", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "rand", "serde", @@ -1808,7 +1817,7 @@ name = "install" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", @@ -1910,6 +1919,29 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "kinode_process_lib" +version = "0.10.1" +source = "git+https://github.com/kinode-dao/process_lib?rev=11630ce#11630cea54999548fec610ee41cd318cc6ab60b3" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "bincode", + "http", + "mime_guess", + "rand", + "regex", + "rmp-serde", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", + "wit-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2452,6 +2484,29 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3260,7 +3315,7 @@ name = "uninstall" version = "0.1.0" dependencies = [ "anyhow", - "kinode_process_lib", + "kinode_process_lib 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "process_macros", "serde", "serde_json", diff --git a/kinode/packages/app-store/api/app-store:sys-v1.wit b/kinode/packages/app-store/api/app-store:sys-v1.wit index d3f98873a..8d83b34f1 100644 --- a/kinode/packages/app-store/api/app-store:sys-v1.wit +++ b/kinode/packages/app-store/api/app-store:sys-v1.wit @@ -135,6 +135,10 @@ interface chain { /// /// lazy-load-blob: none. stop-auto-update(package-id), + /// Get reviews for an app + /// + /// lazy-load-blob: none. + get-reviews(package-id), /// Reset app-store db /// /// lazy-load-blob: none. @@ -153,6 +157,8 @@ interface chain { auto-update-started, /// lazy-load-blob: none. auto-update-stopped, + /// lazy-load-blob: none + get-reviews(list), /// lazy-load-blob: none. /// successful reset reset-ok, @@ -196,6 +202,14 @@ interface chain { wit-version: option, dependencies: option>, } + + /// Review associated with an on-chain app + record onchain-review { + reviewer: string, + stars: option, + review: option, + block: u64, + } } /// downloads:app-store:sys diff --git a/kinode/packages/app-store/app-store/src/http_api.rs b/kinode/packages/app-store/app-store/src/http_api.rs index 40713a371..fab1e2bf5 100644 --- a/kinode/packages/app-store/app-store/src/http_api.rs +++ b/kinode/packages/app-store/app-store/src/http_api.rs @@ -35,6 +35,7 @@ pub fn init_frontend(our: &Address, http_server: &mut server::HttpServer) { "/apps/:id", // detail about an on-chain app "/downloads/:id", // local downloads for an app "/installed/:id", // detail about an installed app + "/reviews/:id", // reviews for an app "/manifest", // manifest of a downloaded app, id & version hash in query params // actions "/apps/:id/download", // download a listed app @@ -734,6 +735,27 @@ fn serve_paths( )), } } + "/reviews/:id" => { + let Ok(package_id) = get_package_id(url_params) else { + return Ok(( + StatusCode::BAD_REQUEST, + None, + format!("Missing id").into_bytes(), + )); + }; + let resp = Request::to(("our", "chain", "app-store", "sys")) + .body(&ChainRequest::GetReviews( + crate::kinode::process::main::PackageId::from_process_lib(package_id), + )) + .send_and_await_response(5)??; + let msg = serde_json::from_slice::(resp.body())?; + match msg { + ChainResponse::GetReviews(reviews) => { + Ok((StatusCode::OK, None, serde_json::to_vec(&reviews)?)) + } + _ => Err(anyhow::anyhow!("Invalid response from chain: {:?}", msg)), + } + } // GET all failed/pending auto_updates "/updates" => { let serialized = serde_json::to_vec(&updates).unwrap_or_default(); diff --git a/kinode/packages/app-store/chain/Cargo.toml b/kinode/packages/app-store/chain/Cargo.toml index da6c94014..33a94e6fe 100644 --- a/kinode/packages/app-store/chain/Cargo.toml +++ b/kinode/packages/app-store/chain/Cargo.toml @@ -11,7 +11,7 @@ alloy-primitives = "0.8.15" alloy-sol-types = "0.8.15" anyhow = "1.0" bincode = "1.3.3" -kinode_process_lib = "0.10.1" +kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "11630ce" } process_macros = "0.1" rand = "0.8" serde = { version = "1.0", features = ["derive"] } diff --git a/kinode/packages/app-store/chain/src/lib.rs b/kinode/packages/app-store/chain/src/lib.rs index deeed73a2..843ee1f59 100644 --- a/kinode/packages/app-store/chain/src/lib.rs +++ b/kinode/packages/app-store/chain/src/lib.rs @@ -26,15 +26,19 @@ //! metadata management and providing information about available apps. //! use crate::kinode::process::chain::{ - ChainError, ChainRequest, OnchainApp, OnchainMetadata, OnchainProperties, + ChainError, ChainRequest, OnchainApp, OnchainMetadata, OnchainProperties, OnchainReview, }; use crate::kinode::process::downloads::{AutoUpdateRequest, DownloadRequest}; use alloy_primitives::keccak256; -use alloy_sol_types::SolEvent; +use alloy_sol_types::{sol, SolCall, SolEvent}; use kinode::process::chain::ChainResponse; use kinode_process_lib::{ - await_message, call_init, eth, get_blob, http, kernel_types as kt, kimap, print_to_terminal, - println, + await_message, call_init, + eth::{self, TransactionRequest}, + get_blob, http, kernel_types as kt, + kimap::{self, Note}, + net::get_name, + print_to_terminal, println, sqlite::{self, Sqlite}, timer, Address, Message, PackageId, Request, Response, }; @@ -63,6 +67,15 @@ const KIMAP_ADDRESS: &str = "0x9CE8cCD2932DC727c70f9ae4f8C2b68E6Abed58C"; const DELAY_MS: u64 = 1_000; // 1s +// todo: token addition to the process_lib +sol! { + /// ERC6551 token method. enables us to find the namehash for a TBA. + function token(address account) internal view returns (uint256, address, uint256); + + /// verify a review from the app TBA + function isValidTba(address tba) external view returns (bool); +} + pub struct State { /// the kimap helper we are using pub kimap: kimap::Kimap, @@ -96,6 +109,7 @@ impl DB { inner.write(CREATE_META_TABLE.into(), vec![], None)?; inner.write(CREATE_LISTINGS_TABLE.into(), vec![], None)?; inner.write(CREATE_PUBLISHED_TABLE.into(), vec![], None)?; + inner.write(CREATE_REVIEWS_TABLE.into(), vec![], None)?; Ok(Self { inner }) } @@ -322,6 +336,76 @@ impl DB { } Ok(result) } + + pub fn insert_or_update_review( + &self, + target: &str, + reviewer: &str, + review: Option, + stars: Option, + block: u64, + ) -> anyhow::Result<()> { + // Convert empty review to NULL, otherwise keep as is + let review_param = review + .as_ref() + .and_then(|r| if r.is_empty() { None } else { Some(r) }); + // Convert 0 stars to NULL, otherwise keep as is + let stars_param = stars.and_then(|s| if s == 0 { None } else { Some(s as i64) }); + + let query = "INSERT INTO reviews (target, reviewer, review, stars, block) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(target, reviewer) + DO UPDATE SET + review = CASE + WHEN ? IS NULL THEN reviews.review -- Keep existing if not provided + ELSE ? -- Use new value (which might be NULL) + END, + stars = CASE + WHEN ? IS NULL THEN reviews.stars -- Keep existing if not provided + ELSE ? -- Use new value (which might be NULL) + END, + block = ?"; + + let params = vec![ + target.into(), + reviewer.into(), + review_param.map(|s| s.to_string()).into(), + stars_param.into(), + block.into(), + // For review CASE + review.is_some().into(), + review_param.map(|s| s.to_string()).into(), + // For stars CASE + stars.is_some().into(), + stars_param.into(), + // Block update + block.into(), + ]; + + self.inner.write(query.into(), params, None)?; + Ok(()) + } + + pub fn get_reviews(&self, target: &str) -> anyhow::Result> { + let query = "SELECT reviewer, review, stars, block FROM reviews WHERE target = ?"; + let params = vec![target.into()]; + let rows = self.inner.read(query.into(), params)?; + let mut reviews = Vec::new(); + for row in rows { + let reviewer = row["reviewer"].as_str().unwrap_or("").to_string(); + let review = row["review"].as_str().map(|s| s.to_string()); + let stars = row["stars"].as_i64().map(|s| s as u8); + let block = row["block"].as_i64().unwrap_or(0) as u64; + + reviews.push(OnchainReview { + reviewer, + review, + stars, + block, + }); + } + Ok(reviews) + } } const CREATE_META_TABLE: &str = " @@ -350,6 +434,16 @@ CREATE TABLE IF NOT EXISTS published ( PRIMARY KEY (package_name, publisher_node) );"; +const CREATE_REVIEWS_TABLE: &str = " +CREATE TABLE IF NOT EXISTS reviews ( + target TEXT NOT NULL, + reviewer TEXT NOT NULL, + review TEXT, + stars INTEGER CHECK (stars >= 0 AND stars <= 5), + block INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (target, reviewer) +);"; + call_init!(init); fn init(our: Address) { loop { @@ -393,7 +487,7 @@ fn init(our: Address) { /// returns true if we should re-index fn handle_message(our: &Address, state: &mut State, message: &Message) -> anyhow::Result { - if !message.is_local(&our) { + if !message.is_local() { // networking is off: we will never get non-local messages return Ok(false); } @@ -475,7 +569,7 @@ fn handle_local_request(state: &mut State, req: ChainRequest) -> anyhow::Result< } } ChainRequest::StopAutoUpdate(package_id) => { - let pid = package_id.to_process_lib(); + let pid: PackageId = package_id.to_process_lib(); if let Some(mut listing) = state.db.get_listing(&pid)? { listing.auto_update = false; state.db.insert_or_update_listing(&pid, &listing)?; @@ -486,6 +580,14 @@ fn handle_local_request(state: &mut State, req: ChainRequest) -> anyhow::Result< Response::new().body(&error_response).send()?; } } + ChainRequest::GetReviews(package_id) => { + let pid: PackageId = package_id.to_process_lib(); + let target = format!("{}.{}", pid.package_name, pid.publisher_node); + + let reviews = state.db.get_reviews(&target)?; + let response = ChainResponse::GetReviews(reviews); + Response::new().body(&response).send()?; + } ChainRequest::Reset => { // state.db.reset(&our); Response::new().body(&ChainResponse::ResetOk).send()?; @@ -510,6 +612,32 @@ fn handle_eth_log( return Ok(()); }; + if note.note == "~review" || note.note == "~stars" { + let (reviewer, target) = note + .parent_path + .split_once('.') + .ok_or(anyhow::anyhow!("invalid reviewer name")) + .and_then(|(reviewer, target)| { + if reviewer.is_empty() || target.is_empty() { + Err(anyhow::anyhow!("invalid reviewer name")) + } else { + Ok((reviewer, target)) + } + })?; + + // get the hex encoded tba from "review-tba." + let reviewer_hex = reviewer + .strip_prefix("review-") + .ok_or(anyhow::anyhow!("invalid reviewer prefix"))?; + + // parse the hex string into an eth::Address + let reviewer_address = eth::Address::from_str(&format!("0x{}", reviewer_hex)) + .map_err(|e| anyhow::anyhow!("invalid reviewer address: {}", e))?; + + handle_review_or_stars(state, target, reviewer_address, ¬e, block_number)?; + return Ok(()); + } + let package_id = note .parent_path .split_once('.') @@ -624,6 +752,65 @@ fn handle_eth_log( Ok(()) } +/// handle review or stars +/// verifies that the reviewer is a TBA +/// gets the reviewer's name +fn handle_review_or_stars( + state: &mut State, + target: &str, + reviewer_address: eth::Address, + note: &Note, + block_number: u64, +) -> anyhow::Result<()> { + // Verify TBA and get reviewer name + let (target_tba, _, _) = state.kimap.get(&target)?; + let verify_call = isValidTbaCall { + tba: reviewer_address, + } + .abi_encode(); + let tx = TransactionRequest::default() + .to(target_tba) + .input(verify_call.into()); + let result = state.kimap.provider.call(tx, None)?; + let is_valid_review = isValidTbaCall::abi_decode_returns(&result, false)?._0; + if !is_valid_review { + return Ok(()); + } + + // Get reviewer name + let reviewer_namehash = state.kimap.get_namehash_from_tba(reviewer_address)?; + let Some(reviewer_name) = get_name(&reviewer_namehash, None, None) else { + return Ok(()); + }; + + // Handle review or stars + match note.note.as_str() { + "~review" => { + let review = String::from_utf8_lossy(¬e.data).to_string(); + state.db.insert_or_update_review( + &target, + &reviewer_name, + Some(review), + None, + block_number, + )?; + } + "~stars" => { + let stars = if note.data.len() != 1 { + return Err(anyhow::anyhow!("Invalid stars data length")); + } else { + Some(note.data[0]) + }; + state + .db + .insert_or_update_review(&target, &reviewer_name, None, stars, block_number)?; + } + _ => return Ok(()), + } + + Ok(()) +} + /// after startup, fetch metadata for all listings /// we do this as a separate step to not repeatedly fetch outdated metadata /// as we process logs. @@ -739,13 +926,18 @@ fn update_all_metadata(state: &mut State, last_saved_block: u64) { } /// create the filter used for app store getLogs and subscription. -/// the app store exclusively looks for ~metadata-uri postings: if one is -/// observed, we then *query* for ~metadata-hash to verify the content -/// at the URI. /// -/// this means that ~metadata-hash should be *posted before or at the same time* as ~metadata-uri! +/// we're looking for three types of notes: +/// +/// 1. ~metadata-uri: a URI pointing to the metadata for a package (~metadata-hash is manually fetched here.) +/// 2. ~review: a user review of a package +/// 3. ~stars: a user's rating of a package pub fn app_store_filter(state: &State) -> eth::Filter { - let notes = vec![keccak256("~metadata-uri")]; + let notes = vec![ + keccak256("~metadata-uri"), + keccak256("~review"), + keccak256("~stars"), + ]; eth::Filter::new() .address(*state.kimap.address()) diff --git a/kinode/packages/app-store/ui/src/abis/helpers.ts b/kinode/packages/app-store/ui/src/abis/helpers.ts index 9917108e6..954c1d976 100644 --- a/kinode/packages/app-store/ui/src/abis/helpers.ts +++ b/kinode/packages/app-store/ui/src/abis/helpers.ts @@ -1,4 +1,4 @@ -import { multicallAbi, kimapAbi, mechAbi, KIMAP, MULTICALL, KINO_ACCOUNT_IMPL } from "./"; +import { multicallAbi, kimapAbi, mechAbi, KIMAP, MULTICALL, REVIEW_IMPL } from "./"; import { encodeFunctionData, encodePacked, stringToHex } from "viem"; export function encodeMulticalls(metadataUri: string, metadataHash: string) { @@ -53,7 +53,7 @@ export function encodeIntoMintCall(multicalls: `0x${string}`, our_address: `0x${ encodePacked(["bytes"], [stringToHex(app_name)]), initCall, "0x", - KINO_ACCOUNT_IMPL, + REVIEW_IMPL, ] }) return mintCall; diff --git a/kinode/packages/app-store/ui/src/abis/index.ts b/kinode/packages/app-store/ui/src/abis/index.ts index dea088c86..3b43526f6 100644 --- a/kinode/packages/app-store/ui/src/abis/index.ts +++ b/kinode/packages/app-store/ui/src/abis/index.ts @@ -4,7 +4,7 @@ export { encodeMulticalls, encodeIntoMintCall } from "./helpers"; export const KIMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658"; export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11"; -export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716"; +export const REVIEW_IMPL: `0x${string}` = "0x000000E09418Ee5B467e81cf1B250FF4FB660023"; export const multicallAbi = parseAbi([ diff --git a/kinode/packages/app-store/ui/src/pages/AppPage.tsx b/kinode/packages/app-store/ui/src/pages/AppPage.tsx index e1005efc8..a50871bde 100644 --- a/kinode/packages/app-store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app-store/ui/src/pages/AppPage.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState, useCallback, useMemo } from "react"; import { useParams } from "react-router-dom"; -import { FaDownload, FaCheck, FaTimes, FaPlay, FaSpinner, FaTrash, FaSync, FaChevronDown, FaChevronUp } from "react-icons/fa"; +import { FaDownload, FaCheck, FaTimes, FaPlay, FaSpinner, FaTrash, FaSync, FaChevronDown, FaChevronUp, FaStar } from "react-icons/fa"; import useAppsStore from "../store"; -import { AppListing, PackageState, ManifestResponse } from "../types/Apps"; +import { AppListing, PackageState, ManifestResponse, Review } from "../types/Apps"; import { compareVersions } from "../utils/compareVersions"; import { MirrorSelector, ManifestDisplay } from '../components'; @@ -20,7 +20,9 @@ export default function AppPage() { downloads, activeDownloads, installApp, - clearAllActiveDownloads + clearAllActiveDownloads, + fetchReviews, + reviews } = useAppsStore(); const [app, setApp] = useState(null); @@ -41,6 +43,7 @@ export default function AppPage() { const [manifestResponse, setManifestResponse] = useState(null); const [canLaunch, setCanLaunch] = useState(false); const [attemptedDownload, setAttemptedDownload] = useState(false); + const [showReviews, setShowReviews] = useState(false); const appDownloads = useMemo(() => downloads[id || ""] || [], [downloads, id]); @@ -80,7 +83,8 @@ export default function AppPage() { try { const [appData, installedAppData] = await Promise.all([ fetchListing(id), - fetchInstalledApp(id) + fetchInstalledApp(id), + fetchReviews(id) ]); if (!appData) { @@ -116,7 +120,7 @@ export default function AppPage() { } finally { setIsLoading(false); } - }, [id, fetchListing, fetchInstalledApp, fetchHomepageApps, getLaunchUrl]); + }, [id, fetchListing, fetchInstalledApp, fetchReviews, fetchHomepageApps, getLaunchUrl]); const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => { setSelectedMirror(mirror); @@ -138,6 +142,14 @@ export default function AppPage() { const downloads = await fetchDownloadsForApp(id); const download = downloads.find(d => d.File?.name === `${versionData.hash}.zip`); + + // // If not found, try 3 more times with 500ms delays + // for (let i = 0; i < 3 && !download?.File?.manifest; i++) { + // await new Promise(resolve => setTimeout(resolve, 2200)); + // downloads = await fetchDownloadsForApp(id); + // download = downloads.find(d => d.File?.name === `${versionData.hash}.zip`); + // } + if (download?.File?.manifest) { const manifest_response: ManifestResponse = { package_id: app.package_id, @@ -422,6 +434,42 @@ export default function AppPage() { {app.metadata?.description || "No description available"} + +
+ {reviews[id || '']?.length > 0 ? ( +
+ {reviews[id || '']?.map((review, index) => ( +
+
+ {review.reviewer} + {review.stars && ( +
+ {[...Array(review.stars)].map((_, i) => ( + + ))} +
+ )} +
+
{review.review || 'No written review'}
+
+ ))} +
+ ) : ( +
No reviews yet
+ )} +
+ {valid_wit_version && !upToDate && ( <>