diff --git a/.github/workflows/api-deploy.yml b/.github/workflows/api-deploy.yml index 0fca8dda..6f77786b 100644 --- a/.github/workflows/api-deploy.yml +++ b/.github/workflows/api-deploy.yml @@ -94,7 +94,6 @@ jobs: runs-on: ubuntu-latest needs: Build steps: - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -117,9 +116,14 @@ jobs: environment-variables: | RUST_LOG=INFO LOG_LEVEL=normal + ENVIRONMENT=dev + METRICS_PORT=8001 VITE_URL=https://cairo-remix-dev.nethermind.io + SERVICE_VERSION=v${{ needs.Build.outputs.image-version }} PROMTAIL_USERNAME=${{secrets.PROMTAIL_USERNAME}} PROMTAIL_PASSWORD=${{secrets.PROMTAIL_PASSWORD}} + PROMETHEUS_USERNAME=${{secrets.PROMETHEUS_USERNAME}} + PROMETHEUS_PASSWORD=${{secrets.PROMETHEUS_PASSWORD}} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 @@ -133,7 +137,6 @@ jobs: runs-on: ubuntu-latest needs: Build steps: - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -156,9 +159,14 @@ jobs: environment-variables: | RUST_LOG=INFO LOG_LEVEL=normal + ENVIRONMENT=prod + METRICS_PORT=8001 VITE_URL=https://cairo-remix-dev.nethermind.io + SERVICE_VERSION=v${{ needs.Build.outputs.image-version }} PROMTAIL_USERNAME=${{secrets.PROMTAIL_USERNAME}} PROMTAIL_PASSWORD=${{secrets.PROMTAIL_PASSWORD}} + PROMETHEUS_USERNAME=${{secrets.PROMETHEUS_USERNAME}} + PROMETHEUS_PASSWORD=${{secrets.PROMETHEUS_PASSWORD}} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 diff --git a/.github/workflows/prod-build-and-deploy.yml b/.github/workflows/prod-build-and-deploy.yml index 5c90e4b0..219400bb 100644 --- a/.github/workflows/prod-build-and-deploy.yml +++ b/.github/workflows/prod-build-and-deploy.yml @@ -132,9 +132,14 @@ jobs: # inject the expected React package URL for CORS logic environment-variables: | RUST_LOG=INFO + ENVIRONMENT=dev + METRICS_PORT=8001 VITE_URL=https://cairo-remix.nethermind.io + SERVICE_VERSION=v${{ needs.Build.outputs.image-version }} PROMTAIL_USERNAME=${{secrets.PROMTAIL_USERNAME}} PROMTAIL_PASSWORD=${{secrets.PROMTAIL_PASSWORD}} + PROMETHEUS_USERNAME=${{secrets.PROMETHEUS_USERNAME}} + PROMETHEUS_PASSWORD=${{secrets.PROMETHEUS_PASSWORD}} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 diff --git a/DockerfileRocket b/DockerfileRocket index a3d2ae6e..3adffb8e 100644 --- a/DockerfileRocket +++ b/DockerfileRocket @@ -5,7 +5,6 @@ ARG SCARB_VERSION ENV DEBIAN_FRONTEND=noninteractive ENV DEBCONF_NONINTERACTIVE_SEEN=true ENV SCARB_VERSION=${SCARB_VERSION} - RUN apt-get clean RUN apt-get update @@ -24,8 +23,7 @@ RUN wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | tee /etc/apt/ RUN echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list RUN apt-get update - -RUN apt-get install grafana-agent +RUN apt-get install grafana-agent-flow WORKDIR /opt/app @@ -35,15 +33,13 @@ SHELL ["/bin/bash", "-lc"] RUN curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | bash -s -- -v $SCARB_VERSION ENV PATH="/root/.local/bin:${PATH}" -# copy Remix plugin source and install Rust -COPY . /opt/app - +# Install Rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable -y - RUN whoami - ENV PATH="/root/.cargo/bin:${PATH}" +# copy Remix plugin source +COPY . /opt/app RUN git submodule update --init # Build the API service diff --git a/api/Cargo.lock b/api/Cargo.lock index 88446c8d..0b9c02bc 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -41,14 +41,22 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + [[package]] name = "api" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "crossbeam-queue", "crossbeam-skiplist", "fmt", + "prometheus", "rocket", "serde", "thiserror", @@ -898,6 +906,27 @@ dependencies = [ "yansi", ] +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "quote" version = "1.0.36" diff --git a/api/Cargo.toml b/api/Cargo.toml index 1f750b5a..a97a76f2 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.91" rocket = { version = "0.5.1", features = ["json"] } serde = { version = "1.0.189", features = ["derive"] } tracing = "^0.1" @@ -17,4 +18,5 @@ crossbeam-queue = "0.3.8" crossbeam-skiplist = "0.1.1" fmt = "0.1.0" thiserror = "1.0.50" -chrono = "0.4.31" \ No newline at end of file +chrono = "0.4.31" +prometheus = "0.13.4" \ No newline at end of file diff --git a/api/configs/grafana-logs.config.river b/api/configs/grafana-logs.config.river new file mode 100644 index 00000000..0334dc0f --- /dev/null +++ b/api/configs/grafana-logs.config.river @@ -0,0 +1,52 @@ +local.file_match "logs" { + path_targets = [{ + __address__ = "localhost", + __path__ = env("PROMTAIL_BASE_DIR") + "/logs/*", + host = "localhost", + job = "starknet-remix-docker-logs", + }] +} + +loki.source.file "logs" { + targets = local.file_match.logs.targets + forward_to = [loki.write.logs.receiver] +} + +loki.write "logs" { + external_labels = { + environment = env("ENVIRONMENT"), + service_version = env("SERVICE_VERSION"), + } + endpoint { + url = "https://logs-prod-us-central1.grafana.net/loki/api/v1/push" + basic_auth { + username = env("PROMTAIL_USERNAME") + password = env("PROMTAIL_PASSWORD") + } + } +} + +prometheus.remote_write "metrics" { + endpoint { + name = "starknet-remix-metrics" + url = "https://prometheus-us-central1.grafana.net/api/prom/push" + + basic_auth { + username = env("PROMETHEUS_USERNAME") + password = env("PROMETHEUS_PASSWORD") + } + } +} + +prometheus.scrape "metrics" { + targets = [{ + __address__ = "localhost:" + env("METRICS_PORT"), + __metrics_path__ = "/metrics", + environment = env("ENVIRONMENT"), + service_version = env("SERVICE_VERSION"), + }] + forward_to = [prometheus.remote_write.metrics.receiver] + + job_name = "starknet-remix-metrics-job" + scrape_interval = "30s" +} diff --git a/api/configs/grafana-logs.config.yaml b/api/configs/grafana-logs.config.yaml deleted file mode 100644 index c66c9a58..00000000 --- a/api/configs/grafana-logs.config.yaml +++ /dev/null @@ -1,17 +0,0 @@ -logs: - configs: - - name: default - positions: - filename: /tmp/positions.yaml - scrape_configs: - - job_name: starknet-remix-docker-logs - static_configs: - - targets: [localhost] - labels: - job: starknet-remix-docker-logs - __path__: "${PROMTAIL_BASE_DIR}/logs/*" - clients: - - url: https://logs-prod-us-central1.grafana.net/loki/api/v1/push - basic_auth: - username: ${PROMTAIL_USERNAME} - password: ${PROMTAIL_PASSWORD} diff --git a/api/docker_run.sh b/api/docker_run.sh index 81f03d82..61bdc0d4 100755 --- a/api/docker_run.sh +++ b/api/docker_run.sh @@ -2,8 +2,18 @@ # Note: This script needs to run from inside /api dir export PROMTAIL_BASE_DIR=$(pwd) +export METRICS_PORT=${METRICS_PORT-8001} +if [ -z ${ENVIRONMENT} ]; then + echo "ENVIRONMENT env var undefined" + exit 1 +fi -grafana-agent --config.expand-env=true --config.file ./configs/grafana-logs.config.yaml & +if [ -z ${SERVICE_VERSION} ]; then + echo "SERVICE_VERSION env var undefined" + exit 1 +fi + +grafana-agent-flow run ./configs/grafana-logs.config.river & cargo run --release \ No newline at end of file diff --git a/api/example.env b/api/example.env index b18e861e..8fdd797a 100644 --- a/api/example.env +++ b/api/example.env @@ -1,2 +1,6 @@ +VITE_URL=http://localhost:3000 +METRICS_PORT=8001 PROMTAIL_USERNAME=LOKI_Grafana_Username -PROMTAIL_PASSWORD=LOKI_Grafana_password_API_Key \ No newline at end of file +PROMTAIL_PASSWORD=LOKI_Grafana_password_API_Key +PROMETHEUS_USERNAME=PROMETHEUS_Grafana_Username +PROMETHEUS_PASSWORD=PROMETHEUS_Grafana_password_API_Key \ No newline at end of file diff --git a/api/src/types.rs b/api/src/errors.rs similarity index 95% rename from api/src/types.rs rename to api/src/errors.rs index 0e8a891c..a7d1db9f 100644 --- a/api/src/types.rs +++ b/api/src/errors.rs @@ -1,7 +1,5 @@ use std::io::Error as IoError; -pub type Result = std::result::Result; - #[derive(Debug, thiserror::Error)] pub enum ApiError { #[error("Failed to execute command: {0}")] @@ -35,3 +33,5 @@ pub enum ApiError { #[error("Error while trying to unlock mutex")] MutexUnlockError, } + +pub type Result = std::result::Result; diff --git a/api/src/handlers/cairo_version.rs b/api/src/handlers/cairo_version.rs index e5b1a36b..152a377e 100644 --- a/api/src/handlers/cairo_version.rs +++ b/api/src/handlers/cairo_version.rs @@ -1,15 +1,18 @@ +use rocket::tokio::fs::read_dir; +use rocket::State; +use std::path::Path; +use std::process::{Command, Stdio}; +use tracing::{error, info, instrument}; + +use crate::errors::{ApiError, Result}; use crate::handlers::process::{do_process_command, fetch_process_result}; use crate::handlers::types::{ApiCommand, ApiCommandResult}; use crate::rate_limiter::RateLimited; -use crate::types::{ApiError, Result}; -use crate::utils::lib::DEFAULT_CAIRO_DIR; +use crate::utils::lib::{CAIRO_COMPILERS_DIR, DEFAULT_CAIRO_DIR}; use crate::worker::WorkerEngine; -use rocket::State; -use std::process::{Command, Stdio}; -use tracing::{error, info, instrument}; // Read the version from the cairo Cargo.toml file. -#[instrument] +#[instrument(skip(_rate_limited))] #[get("/cairo_version")] pub async fn cairo_version(_rate_limited: RateLimited) -> String { info!("/cairo_version"); @@ -17,7 +20,7 @@ pub async fn cairo_version(_rate_limited: RateLimited) -> String { } // Read the version from the cairo Cargo.toml file. -#[instrument] +#[instrument(skip(engine, _rate_limited))] #[get("/cairo_version_async")] pub async fn cairo_version_async( engine: &State, @@ -27,7 +30,7 @@ pub async fn cairo_version_async( do_process_command(ApiCommand::CairoVersion, engine) } -#[instrument] +#[instrument(skip(engine))] #[get("/cairo_version_result/")] pub async fn get_cairo_version_result(process_id: String, engine: &State) -> String { fetch_process_result(process_id, engine, |result| match result { @@ -66,3 +69,29 @@ pub fn do_cairo_version() -> Result { } } } + +#[instrument] +#[get("/cairo_versions")] +pub async fn cairo_versions() -> String { + do_cairo_versions() + .await + .unwrap_or_else(|e| format!("Failed to get cairo versions: {:?}", e)) +} + +/// Get cairo versions +pub async fn do_cairo_versions() -> crate::errors::Result { + let path = Path::new(CAIRO_COMPILERS_DIR); + + let mut dir = read_dir(path).await.map_err(ApiError::FailedToReadDir)?; + let mut result = vec![]; + + while let Ok(Some(entry)) = dir.next_entry().await { + let entry = entry; + let path = entry.path(); + if path.is_dir() { + result.push(entry.file_name().to_string_lossy().to_string()); + } + } + + Ok(format!("{:?}", result)) +} diff --git a/api/src/handlers/cairo_versions.rs b/api/src/handlers/cairo_versions.rs deleted file mode 100644 index c1904507..00000000 --- a/api/src/handlers/cairo_versions.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::types::{ApiError, Result}; -use crate::utils::lib::CAIRO_COMPILERS_DIR; -use rocket::tokio::fs::read_dir; -use std::path::Path; -use tracing::instrument; - -#[instrument] -#[get("/cairo_versions")] -pub async fn cairo_versions() -> String { - do_cairo_versions() - .await - .unwrap_or_else(|e| format!("Failed to get cairo versions: {:?}", e)) -} - -/// Get cairo versions -pub async fn do_cairo_versions() -> Result { - let path = Path::new(CAIRO_COMPILERS_DIR); - - let mut dir = read_dir(path).await.map_err(ApiError::FailedToReadDir)?; - let mut result = vec![]; - - while let Ok(Some(entry)) = dir.next_entry().await { - let entry = entry; - let path = entry.path(); - if path.is_dir() { - result.push(entry.file_name().to_string_lossy().to_string()); - } - } - - Ok(format!("{:?}", result)) -} diff --git a/api/src/handlers/compile_casm.rs b/api/src/handlers/compile_casm.rs index f0655342..7b112a84 100644 --- a/api/src/handlers/compile_casm.rs +++ b/api/src/handlers/compile_casm.rs @@ -1,9 +1,3 @@ -use crate::handlers::process::{do_process_command, fetch_process_result}; -use crate::handlers::types::{ApiCommand, ApiCommandResult, CompileResponse}; -use crate::rate_limiter::RateLimited; -use crate::types::{ApiError, Result}; -use crate::utils::lib::{get_file_ext, get_file_path, CAIRO_COMPILERS_DIR, CASM_ROOT}; -use crate::worker::WorkerEngine; use rocket::fs::NamedFile; use rocket::serde::json; use rocket::serde::json::Json; @@ -13,27 +7,41 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use tracing::{debug, info, instrument}; -#[instrument] +use crate::errors::{ApiError, Result}; +use crate::handlers::process::{do_process_command, fetch_process_result}; +use crate::handlers::types::{ApiCommand, ApiCommandResult, CompileResponse}; +use crate::handlers::utils::do_metered_action; +use crate::metrics::COMPILATION_LABEL_VALUE; +use crate::rate_limiter::RateLimited; +use crate::utils::lib::{get_file_ext, get_file_path, CAIRO_COMPILERS_DIR, CASM_ROOT}; +use crate::worker::WorkerEngine; + +#[instrument(skip(engine, _rate_limited))] #[get("/compile-to-casm//")] pub async fn compile_to_casm( version: String, remix_file_path: PathBuf, + engine: &State, _rate_limited: RateLimited, ) -> Json { info!("/compile-to-casm/{:?}", remix_file_path); - do_compile_to_casm(version.clone(), remix_file_path) - .await - .unwrap_or_else(|e| { - Json(CompileResponse { - file_content: "".to_string(), - message: format!("Failed to compile to casm: {:?}", e), - status: "CompilationFailed".to_string(), - cairo_version: version, - }) + do_metered_action( + do_compile_to_casm(version.clone(), remix_file_path), + COMPILATION_LABEL_VALUE, + &engine.metrics, + ) + .await + .unwrap_or_else(|e| { + Json(CompileResponse { + file_content: "".to_string(), + message: format!("Failed to compile to casm: {:?}", e), + status: "CompilationFailed".to_string(), + cairo_version: version, }) + }) } -#[instrument] +#[instrument(skip(engine, _rate_limited))] #[get("/compile-to-casm-async//")] pub async fn compile_to_casm_async( version: String, @@ -51,7 +59,7 @@ pub async fn compile_to_casm_async( ) } -#[instrument] +#[instrument(skip(engine))] #[get("/compile-to-casm-result/")] pub async fn compile_to_casm_result(process_id: String, engine: &State) -> String { info!("/compile-to-casm-result/{:?}", process_id); diff --git a/api/src/handlers/compile_sierra.rs b/api/src/handlers/compile_sierra.rs index c9c3ac25..f7bc8239 100644 --- a/api/src/handlers/compile_sierra.rs +++ b/api/src/handlers/compile_sierra.rs @@ -1,7 +1,7 @@ +use crate::errors::{ApiError, Result}; use crate::handlers::process::{do_process_command, fetch_process_result}; use crate::handlers::types::{ApiCommand, ApiCommandResult, CompileResponse}; use crate::rate_limiter::RateLimited; -use crate::types::{ApiError, Result}; use crate::utils::lib::{get_file_ext, get_file_path, CAIRO_COMPILERS_DIR, SIERRA_ROOT}; use crate::worker::WorkerEngine; use rocket::fs::NamedFile; @@ -13,7 +13,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use tracing::{debug, info, instrument}; -#[instrument] +#[instrument(skip(_rate_limited))] #[get("/compile-to-sierra//")] pub async fn compile_to_sierra( version: String, @@ -35,7 +35,7 @@ pub async fn compile_to_sierra( } } -#[instrument] +#[instrument(skip(engine, _rate_limited))] #[get("/compile-to-sierra-async//")] pub async fn compile_to_siera_async( version: String, @@ -53,7 +53,7 @@ pub async fn compile_to_siera_async( ) } -#[instrument] +#[instrument(skip(engine))] #[get("/compile-to-sierra-result/")] pub async fn get_siera_compile_result(process_id: String, engine: &State) -> String { info!("/compile-to-sierra-result"); diff --git a/api/src/handlers/mod.rs b/api/src/handlers/mod.rs index 6ddfcea7..aeb506c1 100644 --- a/api/src/handlers/mod.rs +++ b/api/src/handlers/mod.rs @@ -1,5 +1,4 @@ pub mod cairo_version; -pub mod cairo_versions; pub mod compile_casm; pub mod compile_sierra; pub mod process; @@ -7,18 +6,22 @@ pub mod save_code; pub mod scarb_compile; pub mod scarb_test; pub mod types; +pub mod utils; +use rocket::serde::json::Json; +use std::path::Path; +use tracing::info; +use tracing::instrument; + +use crate::errors::{ApiError, Result}; use crate::handlers::cairo_version::do_cairo_version; use crate::handlers::compile_casm::do_compile_to_casm; use crate::handlers::compile_sierra::do_compile_to_sierra; use crate::handlers::scarb_compile::do_scarb_compile; use crate::handlers::scarb_test::do_scarb_test; use crate::handlers::types::{ApiCommand, ApiCommandResult, FileContentMap}; -use crate::types::{ApiError, Result}; -use rocket::serde::json::Json; -use std::path::Path; -use tracing::info; -use tracing::instrument; +use crate::handlers::utils::do_metered_action; +use crate::metrics::{Metrics, COMPILATION_LABEL_VALUE}; #[instrument] #[get("/health")] @@ -34,14 +37,20 @@ pub async fn who_is_this() -> &'static str { "Who are you?" } -pub async fn dispatch_command(command: ApiCommand) -> Result { +pub async fn dispatch_command(command: ApiCommand, metrics: &Metrics) -> Result { match command { ApiCommand::CairoVersion => match do_cairo_version() { Ok(result) => Ok(ApiCommandResult::CairoVersion(result)), Err(e) => Err(e), }, ApiCommand::ScarbCompile { remix_file_path } => { - match do_scarb_compile(remix_file_path).await { + match do_metered_action( + do_scarb_compile(remix_file_path), + COMPILATION_LABEL_VALUE, + metrics, + ) + .await + { Ok(result) => Ok(ApiCommandResult::ScarbCompile(result.into_inner())), Err(e) => Err(e), } @@ -58,7 +67,13 @@ pub async fn dispatch_command(command: ApiCommand) -> Result { ApiCommand::CasmCompile { remix_file_path, version, - } => match do_compile_to_casm(version, remix_file_path).await { + } => match do_metered_action( + do_compile_to_casm(version, remix_file_path), + COMPILATION_LABEL_VALUE, + metrics, + ) + .await + { Ok(Json(compile_response)) => Ok(ApiCommandResult::CasmCompile(compile_response)), Err(e) => Err(e), }, diff --git a/api/src/handlers/process.rs b/api/src/handlers/process.rs index a64a8dbb..60132de1 100644 --- a/api/src/handlers/process.rs +++ b/api/src/handlers/process.rs @@ -4,7 +4,7 @@ use rocket::State; use tracing::{info, instrument}; use uuid::Uuid; -#[instrument] +#[instrument(skip(engine))] #[get("/process_status/")] pub async fn get_process_status(process_id: String, engine: &State) -> String { info!("/process_status/{:?}", process_id); diff --git a/api/src/handlers/save_code.rs b/api/src/handlers/save_code.rs index fcf234bc..c7b3bca3 100644 --- a/api/src/handlers/save_code.rs +++ b/api/src/handlers/save_code.rs @@ -1,11 +1,12 @@ -use crate::types::{ApiError, Result}; +use crate::errors::{ApiError, Result}; use crate::utils::lib::get_file_path; use rocket::data::ToByteUnit; use rocket::tokio::fs; use rocket::Data; use std::path::PathBuf; -use tracing::info; +use tracing::{info, instrument}; +#[instrument(skip(file))] #[post("/save_code/", data = "")] pub async fn save_code(file: Data<'_>, remix_file_path: PathBuf) -> String { info!("/save_code/{:?}", remix_file_path); diff --git a/api/src/handlers/scarb_compile.rs b/api/src/handlers/scarb_compile.rs index e3b4a169..2499c6f8 100644 --- a/api/src/handlers/scarb_compile.rs +++ b/api/src/handlers/scarb_compile.rs @@ -1,10 +1,3 @@ -use crate::handlers::get_files_recursive; -use crate::handlers::process::{do_process_command, fetch_process_result}; -use crate::handlers::types::{ApiCommand, ApiCommandResult, ScarbCompileResponse}; -use crate::rate_limiter::RateLimited; -use crate::types::{ApiError, Result}; -use crate::utils::lib::get_file_path; -use crate::worker::WorkerEngine; use rocket::serde::json; use rocket::serde::json::Json; use rocket::State; @@ -12,14 +5,31 @@ use std::path::PathBuf; use std::process::{Command, Stdio}; use tracing::{debug, info, instrument}; -#[instrument] +use crate::errors::{ApiError, Result}; +use crate::handlers::get_files_recursive; +use crate::handlers::process::{do_process_command, fetch_process_result}; +use crate::handlers::types::{ApiCommand, ApiCommandResult, ScarbCompileResponse}; +use crate::handlers::utils::do_metered_action; +use crate::metrics::COMPILATION_LABEL_VALUE; +use crate::rate_limiter::RateLimited; +use crate::utils::lib::get_file_path; +use crate::worker::WorkerEngine; + +#[instrument(skip(engine, _rate_limited))] #[get("/compile-scarb/")] pub async fn scarb_compile( remix_file_path: PathBuf, + engine: &State, _rate_limited: RateLimited, ) -> Json { info!("/compile-scarb/{:?}", remix_file_path); - do_scarb_compile(remix_file_path).await.unwrap_or_else(|e| { + do_metered_action( + do_scarb_compile(remix_file_path), + COMPILATION_LABEL_VALUE, + &engine.metrics, + ) + .await + .unwrap_or_else(|e| { Json(ScarbCompileResponse { file_content_map_array: vec![], message: format!("Failed to compile to scarb: {:?}", e), @@ -28,7 +38,7 @@ pub async fn scarb_compile( }) } -#[instrument] +#[instrument(skip(engine, _rate_limited))] #[get("/compile-scarb-async/")] pub async fn scarb_compile_async( remix_file_path: PathBuf, @@ -39,7 +49,7 @@ pub async fn scarb_compile_async( do_process_command(ApiCommand::ScarbCompile { remix_file_path }, engine) } -#[instrument] +#[instrument(skip(engine))] #[get("/compile-scarb-result/")] pub async fn get_scarb_compile_result(process_id: String, engine: &State) -> String { info!("/compile-scarb-result/{:?}", process_id); diff --git a/api/src/handlers/scarb_test.rs b/api/src/handlers/scarb_test.rs index 3cd1c2f5..e5aac5d4 100644 --- a/api/src/handlers/scarb_test.rs +++ b/api/src/handlers/scarb_test.rs @@ -1,8 +1,8 @@ +use crate::errors::ApiError; +use crate::errors::Result; use crate::handlers::process::{do_process_command, fetch_process_result}; use crate::handlers::types::{ApiCommand, ApiCommandResult, ScarbTestResponse}; use crate::rate_limiter::RateLimited; -use crate::types::ApiError; -use crate::types::Result; use crate::utils::lib::get_file_path; use crate::worker::WorkerEngine; use rocket::serde::json; @@ -12,7 +12,7 @@ use std::path::PathBuf; use std::process::{Command, Stdio}; use tracing::{debug, info, instrument}; -#[instrument] +#[instrument(skip(engine, _rate_limited))] #[get("/scarb-test-async/")] pub async fn scarb_test_async( remix_file_path: PathBuf, @@ -23,7 +23,7 @@ pub async fn scarb_test_async( do_process_command(ApiCommand::ScarbTest { remix_file_path }, engine) } -#[instrument] +#[instrument(skip(engine))] #[get("/scarb-test-result/")] pub async fn get_scarb_test_result(process_id: String, engine: &State) -> String { info!("/scarb-test-result/{:?}", process_id); diff --git a/api/src/handlers/types.rs b/api/src/handlers/types.rs index 39f5750b..5493d5e4 100644 --- a/api/src/handlers/types.rs +++ b/api/src/handlers/types.rs @@ -1,6 +1,10 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; +pub trait Successable { + fn is_successful(&self) -> bool; +} + #[derive(Debug, Deserialize, Serialize)] #[serde(crate = "rocket::serde")] pub struct CompileResponse { @@ -10,6 +14,12 @@ pub struct CompileResponse { pub cairo_version: String, } +impl Successable for CompileResponse { + fn is_successful(&self) -> bool { + self.message == "Success" + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct FileContentMap { pub file_name: String, @@ -23,6 +33,12 @@ pub struct ScarbCompileResponse { pub file_content_map_array: Vec, } +impl Successable for ScarbCompileResponse { + fn is_successful(&self) -> bool { + self.message == "Success" + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct ScarbTestResponse { pub status: String, diff --git a/api/src/handlers/utils.rs b/api/src/handlers/utils.rs new file mode 100644 index 00000000..65f717ed --- /dev/null +++ b/api/src/handlers/utils.rs @@ -0,0 +1,53 @@ +use rocket::serde::json::Json; +use std::future::Future; +use std::time::Instant; +use tracing::instrument; + +use crate::errors::Result; +use crate::handlers::types::Successable; +use crate::metrics::Metrics; + +#[instrument] +#[post("/on-plugin-launched")] +pub async fn on_plugin_launched() { + tracing::info!("/on-plugin-launched"); +} + +pub(crate) async fn do_metered_action( + action: impl Future>>, + action_label_value: &str, + metrics: &Metrics, +) -> Result> { + let start_time = Instant::now(); + let result = action.await; + let elapsed_time = start_time.elapsed().as_secs_f64(); + metrics + .action_duration_seconds + .with_label_values(&[action_label_value]) + .set(elapsed_time); + + match result { + Ok(val) => { + if val.is_successful() { + metrics + .action_successes_total + .with_label_values(&[action_label_value]) + .inc(); + Ok(val) + } else { + metrics + .action_failures_total + .with_label_values(&[action_label_value]) + .inc(); + Ok(val) + } + } + Err(err) => { + metrics + .action_failures_total + .with_label_values(&[action_label_value]) + .inc(); + Err(err) + } + } +} diff --git a/api/src/main.rs b/api/src/main.rs index 6202941c..ae7c16c1 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -2,19 +2,18 @@ extern crate rocket; pub mod cors; +pub mod errors; pub mod handlers; +mod metrics; pub mod rate_limiter; pub mod tracing_log; -pub mod types; pub mod utils; pub mod worker; -use crate::cors::CORS; -use crate::rate_limiter::RateLimiter; -use crate::tracing_log::init_logger; -use crate::worker::WorkerEngine; -use handlers::cairo_version::{cairo_version, cairo_version_async, get_cairo_version_result}; -use handlers::cairo_versions::cairo_versions; +use anyhow::Context; +use handlers::cairo_version::{ + cairo_version, cairo_version_async, cairo_versions, get_cairo_version_result, +}; use handlers::compile_casm::{compile_to_casm, compile_to_casm_async, compile_to_casm_result}; use handlers::compile_sierra::{ compile_to_siera_async, compile_to_sierra, get_siera_compile_result, @@ -23,15 +22,40 @@ use handlers::process::get_process_status; use handlers::save_code::save_code; use handlers::scarb_compile::{get_scarb_compile_result, scarb_compile, scarb_compile_async}; use handlers::scarb_test::{get_scarb_test_result, scarb_test_async}; +use handlers::utils::on_plugin_launched; use handlers::{health, who_is_this}; -use tracing::info; +use prometheus::Registry; +use rocket::{Build, Config, Rocket}; +use std::env; +use std::net::Ipv4Addr; +use tracing::{info, instrument}; + +use crate::cors::CORS; +use crate::metrics::{initialize_metrics, Metrics}; +use crate::rate_limiter::RateLimiter; +use crate::tracing_log::init_logger; +use crate::worker::WorkerEngine; + +fn create_metrics_server(registry: Registry) -> Rocket { + const DEFAULT_PORT: u16 = 8001; + let port = match env::var("METRICS_PORT") { + Ok(val) => val.parse::().unwrap_or(DEFAULT_PORT), + Err(_) => DEFAULT_PORT, + }; + + let config = Config { + port, + address: Ipv4Addr::UNSPECIFIED.into(), + ..Config::default() + }; -#[launch] -async fn rocket() -> _ { - if let Err(err) = init_logger() { - eprintln!("Error initializing logger: {}", err); - } + rocket::custom(config) + .manage(registry) + .mount("/", routes![metrics::metrics]) +} +#[instrument(skip(metrics))] +fn create_app(metrics: Metrics) -> Rocket { let number_of_workers = match std::env::var("WORKER_THREADS") { Ok(v) => v.parse::().unwrap_or(2u32), Err(_) => 2u32, @@ -43,8 +67,7 @@ async fn rocket() -> _ { }; // Launch the worker processes - let mut engine = WorkerEngine::new(number_of_workers, queue_size); - + let mut engine = WorkerEngine::new(number_of_workers, queue_size, metrics.clone()); engine.start(); info!("Number of workers: {}", number_of_workers); @@ -56,6 +79,7 @@ async fn rocket() -> _ { .manage(engine) .attach(CORS) .manage(RateLimiter::new()) + .attach(metrics) .mount( "/", routes![ @@ -78,6 +102,25 @@ async fn rocket() -> _ { who_is_this, get_scarb_test_result, scarb_test_async, + on_plugin_launched, ], ) } + +#[rocket::main] +async fn main() -> anyhow::Result<()> { + init_logger().context("Failed to initialize logger")?; + + let registry = Registry::new(); + let metrics = initialize_metrics(registry.clone()).context("Failed to initialize metrics")?; + + let app_server = create_app(metrics); + let metrics_server = create_metrics_server(registry.clone()); + + let (app_result, metrics_result) = + rocket::tokio::join!(app_server.launch(), metrics_server.launch()); + app_result?; + metrics_result?; + + Ok(()) +} diff --git a/api/src/metrics.rs b/api/src/metrics.rs new file mode 100644 index 00000000..e5bdc664 --- /dev/null +++ b/api/src/metrics.rs @@ -0,0 +1,128 @@ +use prometheus::core::{AtomicF64, AtomicU64, GenericCounter, GenericCounterVec, GenericGaugeVec}; +use prometheus::{Encoder, GaugeVec, IntCounter, IntCounterVec, Opts, Registry, TextEncoder}; +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::http::{Method, Status}; +use rocket::{Data, Request, State}; +use tracing::debug; +use tracing::instrument; + +const NAMESPACE: &str = "starknet_api"; +pub(crate) const COMPILATION_LABEL_VALUE: &str = "compilation"; + +// Action - compile/verify(once supported) +#[derive(Clone, Debug)] +pub struct Metrics { + pub distinct_users_total: GenericCounterVec, + pub plugin_launches_total: GenericCounter, + pub action_total: GenericCounterVec, + pub requests_total: GenericCounter, + pub action_failures_total: GenericCounterVec, + pub action_successes_total: GenericCounterVec, + pub action_duration_seconds: GenericGaugeVec, +} + +#[rocket::async_trait] +impl Fairing for Metrics { + fn info(&self) -> Info { + Info { + name: "Metrics fairing", + kind: Kind::Request, + } + } + + #[instrument(skip(self, req, _data))] + async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { + self.requests_total.inc(); + if let Some(val) = req.client_ip() { + let ip = val.to_string(); + let ip = ip.as_str(); + debug!("Plugin launched by: {}", ip); + debug!("Headers: {:?}", req.headers()); + + self.distinct_users_total.with_label_values(&[ip]).inc(); + } + + match req.method() { + Method::Options => {} + _ => self.update_metrics(req), + } + } +} + +impl Metrics { + fn update_metrics(&self, req: &mut Request<'_>) { + match req.uri().path().as_str() { + "/compile-scarb" + | "/compile-scarb-async" + | "/compile-to-sierra" + | "/compile-to-sierra-async" => self + .action_total + .with_label_values(&[COMPILATION_LABEL_VALUE]) + .inc(), + "/on-plugin-launched" => self.plugin_launches_total.inc(), + _ => {} + } + } +} + +pub(crate) fn initialize_metrics(registry: Registry) -> Result { + const ACTION_LABEL_NAME: &str = "action"; + + let opts = Opts::new("distinct_users_total", "Distinct users total").namespace(NAMESPACE); + let distinct_users_total = IntCounterVec::new(opts, &["ip"])?; + registry.register(Box::new(distinct_users_total.clone()))?; + + let opts = + Opts::new("plugin_launches_total", "Total number plugin launches").namespace(NAMESPACE); + let plugin_launches_total = IntCounter::with_opts(opts)?; + registry.register(Box::new(plugin_launches_total.clone()))?; + + let opts = Opts::new("action_total", "Total number of action runs").namespace(NAMESPACE); + let action_total = IntCounterVec::new(opts, &[ACTION_LABEL_NAME])?; + registry.register(Box::new(action_total.clone()))?; + + // Follow naming conventions for new metrics https://prometheus.io/docs/practices/naming/ + let opts = Opts::new("requests_total", "Number of requests").namespace(NAMESPACE); + let requests_total = IntCounter::with_opts(opts)?; + registry.register(Box::new(requests_total.clone()))?; + + let opts = Opts::new("action_failures_total", "Number of action failures").namespace(NAMESPACE); + let action_failures_total = IntCounterVec::new(opts, &[ACTION_LABEL_NAME])?; + registry.register(Box::new(action_failures_total.clone()))?; + + let opts = + Opts::new("action_successes_total", "Number of action successes").namespace(NAMESPACE); + let action_successes_total = IntCounterVec::new(opts, &[ACTION_LABEL_NAME])?; + registry.register(Box::new(action_successes_total.clone()))?; + + let opts = + Opts::new("action_duration_seconds", "Duration of action in seconds").namespace(NAMESPACE); + let action_duration_seconds = GaugeVec::new(opts, &[ACTION_LABEL_NAME])?; + registry.register(Box::new(action_duration_seconds.clone()))?; + + Ok(Metrics { + distinct_users_total, + plugin_launches_total, + action_total, + requests_total, + action_failures_total, + action_successes_total, + action_duration_seconds, + }) +} + +#[instrument(skip(registry))] +#[get("/metrics")] +pub(crate) async fn metrics(registry: &State) -> Result { + let metric_families = registry.gather(); + let mut buffer = Vec::new(); + let encoder = TextEncoder::new(); + + match encoder.encode(&metric_families, &mut buffer) { + Ok(_) => match String::from_utf8(buffer) { + Ok(val) => Ok(val), + Err(_) => Err((Status::InternalServerError, "Non utf8 metrics".to_string())), + }, + Err(_) => Err((Status::InternalServerError, "Encode error".to_string())), + } +} diff --git a/api/src/rate_limiter.rs b/api/src/rate_limiter.rs index 5b3d42df..07f8846a 100644 --- a/api/src/rate_limiter.rs +++ b/api/src/rate_limiter.rs @@ -1,4 +1,4 @@ -use crate::types::{ApiError, Result}; +use crate::errors::{ApiError, Result}; use crate::utils::lib::timestamp; use crate::worker::Timestamp; use crossbeam_queue::ArrayQueue; diff --git a/api/src/tracing_log.rs b/api/src/tracing_log.rs index e4914ae2..e96cc819 100644 --- a/api/src/tracing_log.rs +++ b/api/src/tracing_log.rs @@ -1,13 +1,11 @@ +use anyhow::Context; use rocket::yansi::Paint; use tracing_appender::rolling; - +use tracing_subscriber::field::MakeExt; use tracing_subscriber::fmt::writer::MakeWriterExt; use tracing_subscriber::registry::LookupSpan; -use tracing_subscriber::{prelude::*, EnvFilter}; - -use tracing_subscriber::field::MakeExt; - use tracing_subscriber::Layer; +use tracing_subscriber::{prelude::*, EnvFilter}; pub enum LogType { Formatted, @@ -99,7 +97,7 @@ pub fn filter_layer(level: LogLevel) -> EnvFilter { tracing_subscriber::filter::EnvFilter::try_new(filter_str).expect("filter string must parse") } -pub fn init_logger() -> Result<(), Box> { +pub fn init_logger() -> anyhow::Result<()> { // Log all `tracing` events to files prefixed with `debug`. // Rolling these files every day let debug_file = rolling::daily("./logs", "debug").with_max_level(tracing::Level::TRACE); @@ -112,12 +110,13 @@ pub fn init_logger() -> Result<(), Box> { let rolling_files = tracing_subscriber::fmt::layer() .json() .with_writer(all_files) - .with_ansi(false); + .with_ansi(false) + .with_filter(filter_layer(LogLevel::Debug)); let log_type = LogType::from(std::env::var("LOG_TYPE").unwrap_or_else(|_| "json".to_string())); let log_level = LogLevel::from( std::env::var("LOG_LEVEL") - .unwrap_or_else(|_| "debug".to_string()) + .unwrap_or_else(|_| "normal".to_string()) .as_str(), ); @@ -128,16 +127,15 @@ pub fn init_logger() -> Result<(), Box> { .with(default_logging_layer()) .with(filter_layer(log_level)), ) - .unwrap(); + .context("Unable to to set LogType::Formatted as default")?; } LogType::Json => { tracing::subscriber::set_global_default( tracing_subscriber::registry() - .with(json_logging_layer()) - .with(filter_layer(log_level)) + .with(json_logging_layer().with_filter(filter_layer(log_level))) .with(rolling_files), ) - .unwrap(); + .context("Unable to to set LogType::Json as default")?; } }; diff --git a/api/src/worker.rs b/api/src/worker.rs index 618e8741..be4776ae 100644 --- a/api/src/worker.rs +++ b/api/src/worker.rs @@ -1,6 +1,7 @@ +use crate::errors::ApiError; use crate::handlers; use crate::handlers::types::{ApiCommand, ApiCommandResult}; -use crate::types::ApiError; +use crate::metrics::Metrics; use crate::utils::lib::DURATION_TO_PURGE; use crossbeam_queue::ArrayQueue; use crossbeam_skiplist::SkipMap; @@ -45,10 +46,11 @@ pub struct WorkerEngine { pub arc_timestamps_to_purge: Arc>, pub is_supervisor_enabled: Arc>, pub supervisor_thread: Arc>>, + pub metrics: Metrics, } impl WorkerEngine { - pub fn new(num_workers: u32, queue_capacity: usize) -> Self { + pub fn new(num_workers: u32, queue_capacity: usize, metrics: Metrics) -> Self { // Create a queue instance let queue: ArrayQueue<(Uuid, ApiCommand)> = ArrayQueue::new(queue_capacity); @@ -76,6 +78,7 @@ impl WorkerEngine { supervisor_thread: Arc::new(None), arc_timestamps_to_purge, is_supervisor_enabled, + metrics, } } @@ -85,8 +88,11 @@ impl WorkerEngine { let arc_clone = self.arc_command_queue.clone(); let arc_states = self.arc_process_states.clone(); let arc_timestamps_to_purge = self.arc_timestamps_to_purge.clone(); + let metrics_copy = self.metrics.clone(); + self.worker_threads.push(tokio::spawn(async move { - WorkerEngine::worker(arc_clone, arc_states, arc_timestamps_to_purge).await; + WorkerEngine::worker(arc_clone, arc_states, arc_timestamps_to_purge, metrics_copy) + .await; })); } @@ -183,6 +189,7 @@ impl WorkerEngine { arc_command_queue: Arc>, arc_process_states: Arc, arc_timestamps_to_purge: Arc>, + metrics: Metrics, ) { info!("Starting worker thread..."); 'worker_loop: loop { @@ -199,7 +206,7 @@ impl WorkerEngine { // update process state arc_process_states.insert(process_id, ProcessState::Running); - match handlers::dispatch_command(command).await { + match handlers::dispatch_command(command, &metrics).await { Ok(result) => { arc_process_states .insert(process_id, ProcessState::Completed(result)); diff --git a/plugin/.eslintrc.json b/plugin/.eslintrc.json index e88df3b5..5604be0d 100644 --- a/plugin/.eslintrc.json +++ b/plugin/.eslintrc.json @@ -12,6 +12,8 @@ }, "plugins": ["react"], "rules": { - "semi": [2, "never"] + "semi": [2, "never"], + "@typescript-eslint/space-before-function-paren": "off", + "@typescript-eslint/member-delimiter-style": "off" } } diff --git a/plugin/package.json b/plugin/package.json index b5e3c531..453a59f7 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -54,6 +54,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", + "prettier": "^3.2.5", "typescript": "^4.9.5", "vite": "^4.5.1", "vite-plugin-checker": "^0.6.2" diff --git a/plugin/pnpm-lock.yaml b/plugin/pnpm-lock.yaml index 34bcd4ed..7147d7c7 100644 --- a/plugin/pnpm-lock.yaml +++ b/plugin/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: eslint-plugin-react: specifier: ^7.33.2 version: 7.33.2(eslint@8.55.0) + prettier: + specifier: ^3.2.5 + version: 3.3.3 typescript: specifier: ^4.9.5 version: 4.9.5 @@ -164,7 +167,7 @@ importers: version: 4.5.1(@types/node@16.18.68)(less@4.2.0)(terser@5.31.5) vite-plugin-checker: specifier: ^0.6.2 - version: 0.6.2(eslint@8.55.0)(meow@8.1.2)(optionator@0.9.3)(typescript@4.9.5)(vite@4.5.1(@types/node@16.18.68)(less@4.2.0)(terser@5.31.5)) + version: 0.6.2(eslint@8.55.0)(optionator@0.9.3)(typescript@4.9.5)(vite@4.5.1(@types/node@16.18.68)(less@4.2.0)(terser@5.31.5)) packages: @@ -1743,15 +1746,9 @@ packages: '@types/lodash@4.14.202': resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} - '@types/minimist@1.2.5': - resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - '@types/node@16.18.68': resolution: {integrity: sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==} - '@types/normalize-package-data@2.4.4': - resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1978,10 +1975,6 @@ packages: resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} engines: {node: '>= 0.4'} - arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - assemblyscript@0.27.22: resolution: {integrity: sha512-6ClobsR4Hxn6K0daYp/+n9qWTqVbpdVeSGSVDqRvUEz66vvFb8atS6nLm+fnQ54JXuXmzLQy0uWYYgB8G59btQ==} engines: {node: '>=16', npm: '>=7'} @@ -2068,14 +2061,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -2198,14 +2183,6 @@ packages: supports-color: optional: true - decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -2511,10 +2488,6 @@ packages: find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2657,10 +2630,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2700,13 +2669,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - - hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2822,10 +2784,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2942,10 +2900,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -2965,10 +2919,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3024,21 +2974,9 @@ packages: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} - map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - - map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - meow@8.1.2: - resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} - engines: {node: '>=10'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3073,10 +3011,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3122,13 +3056,6 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - - normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3181,26 +3108,14 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -3257,6 +3172,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -3286,10 +3206,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - react-clientside-effect@1.2.6: resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -3386,14 +3302,6 @@ packages: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} - read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} - - read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3531,18 +3439,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-license-ids@3.0.18: - resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==} - starknet-abi-forms@0.1.9: resolution: {integrity: sha512-6vuOEEkXtJdHOus+OuMgi7N4Posp/Bv5+5PA0sg4HjL8+5v4AnGuftdid1WBHaEQjd8FlDZyS/NhGAeQ5G9b9A==} engines: {node: '>=14.0.0'} @@ -3649,10 +3545,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -3687,10 +3579,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.18.1: - resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} - engines: {node: '>=10'} - type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -3699,14 +3587,6 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - - type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -3786,9 +3666,6 @@ packages: '@types/react': optional: true - validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - vite-plugin-checker@0.6.2: resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==} engines: {node: '>=14.16'} @@ -3941,10 +3818,6 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -6050,14 +5923,8 @@ snapshots: '@types/lodash@4.14.202': {} - '@types/minimist@1.2.5': - optional: true - '@types/node@16.18.68': {} - '@types/normalize-package-data@2.4.4': - optional: true - '@types/parse-json@4.0.2': {} '@types/prop-types@15.7.11': {} @@ -6345,9 +6212,6 @@ snapshots: is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 - arrify@1.0.1: - optional: true - assemblyscript@0.27.22: dependencies: binaryen: 116.0.0-nightly.20231102 @@ -6436,16 +6300,6 @@ snapshots: callsites@3.1.0: {} - camelcase-keys@6.2.2: - dependencies: - camelcase: 5.3.1 - map-obj: 4.3.0 - quick-lru: 4.0.1 - optional: true - - camelcase@5.3.1: - optional: true - camelcase@6.3.0: {} caniuse-lite@1.0.30001570: {} @@ -6570,15 +6424,6 @@ snapshots: dependencies: ms: 2.1.2 - decamelize-keys@1.1.1: - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - optional: true - - decamelize@1.2.0: - optional: true - deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.0 @@ -7085,12 +6930,6 @@ snapshots: find-root@1.1.0: {} - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - optional: true - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7249,9 +7088,6 @@ snapshots: graphemer@1.4.0: {} - hard-rejection@2.1.0: - optional: true - has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -7289,14 +7125,6 @@ snapshots: dependencies: react-is: 16.13.1 - hosted-git-info@2.8.9: - optional: true - - hosted-git-info@4.1.0: - dependencies: - lru-cache: 6.0.0 - optional: true - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -7403,9 +7231,6 @@ snapshots: is-path-inside@3.0.3: {} - is-plain-obj@1.1.0: - optional: true - is-regex@1.1.4: dependencies: call-bind: 1.0.5 @@ -7524,9 +7349,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kind-of@6.0.3: - optional: true - language-subtag-registry@0.3.22: {} language-tags@1.0.9: @@ -7555,11 +7377,6 @@ snapshots: lines-and-columns@1.2.4: {} - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - optional: true - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -7608,29 +7425,8 @@ snapshots: semver: 5.7.2 optional: true - map-obj@1.0.1: - optional: true - - map-obj@4.3.0: - optional: true - memoize-one@6.0.0: {} - meow@8.1.2: - dependencies: - '@types/minimist': 1.2.5 - camelcase-keys: 6.2.2 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 3.0.3 - read-pkg-up: 7.0.1 - redent: 3.0.0 - trim-newlines: 3.0.1 - type-fest: 0.18.1 - yargs-parser: 20.2.9 - optional: true - merge2@1.4.1: {} micromatch@4.0.5: @@ -7657,13 +7453,6 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimist-options@4.1.0: - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - optional: true - minimist@1.2.8: {} ms@2.1.2: {} @@ -7695,22 +7484,6 @@ snapshots: node-releases@2.0.14: {} - normalize-package-data@2.5.0: - dependencies: - hosted-git-info: 2.8.9 - resolve: 1.22.8 - semver: 5.7.2 - validate-npm-package-license: 3.0.4 - optional: true - - normalize-package-data@3.0.3: - dependencies: - hosted-git-info: 4.1.0 - is-core-module: 2.13.1 - semver: 7.5.4 - validate-npm-package-license: 3.0.4 - optional: true - normalize-path@3.0.0: {} npm-run-path@4.0.1: @@ -7778,27 +7551,14 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - optional: true - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - optional: true - p-locate@5.0.0: dependencies: p-limit: 3.1.0 - p-try@2.2.0: - optional: true - pako@2.1.0: {} parent-module@1.0.1: @@ -7842,6 +7602,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.3.3: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -7869,9 +7631,6 @@ snapshots: queue-microtask@1.2.3: {} - quick-lru@4.0.1: - optional: true - react-clientside-effect@1.2.6(react@18.2.0): dependencies: '@babel/runtime': 7.23.6 @@ -7978,21 +7737,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - read-pkg-up@7.0.1: - dependencies: - find-up: 4.1.0 - read-pkg: 5.2.0 - type-fest: 0.8.1 - optional: true - - read-pkg@5.2.0: - dependencies: - '@types/normalize-package-data': 2.4.4 - normalize-package-data: 2.5.0 - parse-json: 5.2.0 - type-fest: 0.6.0 - optional: true - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -8138,24 +7882,6 @@ snapshots: source-map@0.6.1: optional: true - spdx-correct@3.2.0: - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.18 - optional: true - - spdx-exceptions@2.5.0: - optional: true - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.18 - optional: true - - spdx-license-ids@3.0.18: - optional: true - starknet-abi-forms@0.1.9(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 @@ -8301,9 +8027,6 @@ snapshots: tr46@0.0.3: {} - trim-newlines@3.0.1: - optional: true - ts-api-utils@1.3.0(typescript@4.9.5): dependencies: typescript: 4.9.5 @@ -8334,19 +8057,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.18.1: - optional: true - type-fest@0.20.2: {} type-fest@0.21.3: {} - type-fest@0.6.0: - optional: true - - type-fest@0.8.1: - optional: true - type-fest@2.19.0: {} typed-array-buffer@1.0.0: @@ -8427,13 +8141,7 @@ snapshots: optionalDependencies: '@types/react': 18.2.45 - validate-npm-package-license@3.0.4: - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - optional: true - - vite-plugin-checker@0.6.2(eslint@8.55.0)(meow@8.1.2)(optionator@0.9.3)(typescript@4.9.5)(vite@4.5.1(@types/node@16.18.68)(less@4.2.0)(terser@5.31.5)): + vite-plugin-checker@0.6.2(eslint@8.55.0)(optionator@0.9.3)(typescript@4.9.5)(vite@4.5.1(@types/node@16.18.68)(less@4.2.0)(terser@5.31.5)): dependencies: '@babel/code-frame': 7.23.5 ansi-escapes: 4.3.2 @@ -8455,7 +8163,6 @@ snapshots: vscode-uri: 3.0.8 optionalDependencies: eslint: 8.55.0 - meow: 8.1.2 optionator: 0.9.3 typescript: 4.9.5 @@ -8575,9 +8282,6 @@ snapshots: yaml@1.10.2: {} - yargs-parser@20.2.9: - optional: true - yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/plugin/src/hooks/useRemixClient.ts b/plugin/src/hooks/useRemixClient.ts index 13b42d85..70bd6ab2 100644 --- a/plugin/src/hooks/useRemixClient.ts +++ b/plugin/src/hooks/useRemixClient.ts @@ -1,16 +1,20 @@ import { PluginClient } from '@remixproject/plugin' import { createClient } from '@remixproject/plugin-webview' -import { fetchGitHubFilesRecursively } from '../utils/initial_scarb_codes' +import { + fetchGitHubFilesRecursively, + type RemixFileInfo +} from '../utils/initial_scarb_codes' import { SCARB_VERSION_REF } from '../utils/constants' import axios from 'axios' +import { apiUrl } from '../utils/network' export class RemixClient extends PluginClient { - constructor () { + constructor() { super() this.methods = ['loadFolderFromUrl', 'loadFolderFromGithub'] } - async loadFolderFromUrl (url: string): Promise { + async loadFolderFromUrl(url: string): Promise { try { await this.call('filePanel', 'createWorkspace', 'code-sample', false) // Fetch JSON data from the URL @@ -19,7 +23,12 @@ export class RemixClient extends PluginClient { // Iterate over each file in the folderContent for (const [filePath, fileContent] of Object.entries(folderContent)) { - await this.call('fileManager', 'setFile', filePath, fileContent as string) + await this.call( + 'fileManager', + 'setFile', + filePath, + fileContent as string + ) } console.log('Folder loaded successfully.') @@ -28,7 +37,7 @@ export class RemixClient extends PluginClient { } } - async loadFolderFromGithub (url: string, folderPath: string): Promise { + async loadFolderFromGithub(url: string, folderPath: string): Promise { console.log('loadFolderFromGithub', url, folderPath) try { await this.call('filePanel', 'createWorkspace', 'code-sample', false) @@ -40,7 +49,12 @@ export class RemixClient extends PluginClient { if (file.fileName === 'Scarb.toml') { fileContent = fileContent.concat('\ncasm = true\n') } - await this.call('fileManager', 'setFile', `${file.path}/${file.fileName}`, fileContent) + await this.call( + 'fileManager', + 'setFile', + `${file.path}/${file.fileName}`, + fileContent + ) } } // write Scarb.toml at root level @@ -59,70 +73,91 @@ export class RemixClient extends PluginClient { } const remixClient = createClient(new RemixClient()) -remixClient.onload().then(async () => { - const workspaces = await remixClient.filePanel.getWorkspaces() - - const workspaceLets: Array<{ name: string, isGitRepo: boolean }> = - JSON.parse(JSON.stringify(workspaces)) +async function remixWriteFiles(files: RemixFileInfo[]): Promise { + for (const file of files) { + const filePath = + file?.path + .replace('examples/starknet_multiple_contracts/', '') + .replace('examples/starknet_multiple_contracts', '') ?? '' - if ( - !workspaceLets.some( - (workspaceLet) => workspaceLet.name === 'cairo_scarb_sample' - ) - ) { - await remixClient.filePanel.createWorkspace( - 'cairo_scarb_sample', - true - ) - try { - await remixClient.fileManager.mkdir('hello_world') - } catch (e) { - console.log(e) + let fileContent: string = file?.content ?? '' + if (file != null && file.fileName === 'Scarb.toml') { + fileContent = fileContent.concat('\ncasm = true') } - const exampleRepo = await fetchGitHubFilesRecursively( - 'software-mansion/scarb', - 'examples/starknet_multiple_contracts', - SCARB_VERSION_REF + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + await remixClient.fileManager.writeFile( + `hello_world/${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + filePath + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }/${file?.fileName}`, + fileContent ) + } +} - console.log('exampleRepo', exampleRepo) +remixClient + .onload() + .then(async () => { + const workspaces = await remixClient.filePanel.getWorkspaces() - try { - for (const file of exampleRepo) { - const filePath = file?.path - .replace('examples/starknet_multiple_contracts/', '') - .replace('examples/starknet_multiple_contracts', '') ?? '' + const workspaceLets: Array<{ name: string; isGitRepo: boolean }> = + JSON.parse(JSON.stringify(workspaces)) - let fileContent: string = file?.content ?? '' + if ( + !workspaceLets.some( + (workspaceLet) => workspaceLet.name === 'cairo_scarb_sample' + ) + ) { + await remixClient.filePanel.createWorkspace('cairo_scarb_sample', true) + try { + await remixClient.fileManager.mkdir('hello_world') + } catch (e) { + console.log(e) + } + const exampleRepo = await fetchGitHubFilesRecursively( + 'software-mansion/scarb', + 'examples/starknet_multiple_contracts', + SCARB_VERSION_REF + ) - if (file != null && file.fileName === 'Scarb.toml') { - fileContent = fileContent.concat('\ncasm = true') + try { + console.log('exampleRepo', exampleRepo) + await remixWriteFiles(exampleRepo) + } catch (e) { + if (e instanceof Error) { + await remixClient.call('notification' as any, 'alert', { + id: 'starknetRemixPluginAlert', + title: 'Please check the write file permission', + message: + e.message + '\n' + 'Did you provide the write file permission?' + }) } - - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - await remixClient.fileManager.writeFile( - `hello_world/${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - filePath - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }/${file?.fileName}`, - fileContent - ) + console.log(e) } - } catch (e) { - if (e instanceof Error) { - await remixClient.call('notification' as any, 'alert', { - id: 'starknetRemixPluginAlert', - title: 'Please check the write file permission', - message: e.message + '\n' + 'Did you provide the write file permission?' + + try { + const response = await fetch(`${apiUrl}/on-plugin-launched`, { + method: 'POST', + redirect: 'follow', + headers: { + 'Content-Type': 'application/octet-stream' + } }) + + console.log('on-plugin-launched') + if (!response.ok) { + console.log('Could not post on launch') + } + } catch (error) { + console.log('Could not post on launch', error) } - console.log(e) } - } -}).catch((error) => { - console.error('Error loading remix client:', error) -}) + }) + .catch((error) => { + console.error('Error loading remix client:', error) + }) const useRemixClient = (): { remixClient: typeof remixClient diff --git a/plugin/src/utils/initial_scarb_codes.ts b/plugin/src/utils/initial_scarb_codes.ts index 4e37713c..7881c24e 100644 --- a/plugin/src/utils/initial_scarb_codes.ts +++ b/plugin/src/utils/initial_scarb_codes.ts @@ -1,51 +1,54 @@ import axios from 'axios' -export async function fetchGitHubFilesRecursively ( +export interface RemixFileInfo { + fileName: string + content: string + path: string +} + +export async function fetchGitHubFilesRecursively( repository: string, path: string, ref?: string -): Promise> { +): Promise { const apiUrl = (function () { if (ref === undefined) { return `https://api.github.com/repos/${repository}/contents/${path}` } return `https://api.github.com/repos/${repository}/contents/${path}?ref=${ref}` - }()) + })() try { const response = await axios.get(apiUrl) - if (Array.isArray(response.data)) { - const files = response.data.filter((item) => item.type === 'file') - const fileContents = await Promise.all( - files.map(async (file) => { - if (file.type === 'file') { - const fileResponse = await axios.get(file.download_url) - return { - path, - fileName: file.name, - content: fileResponse.data - } - } - return null - }) - ) - - const subDirectories = response.data.filter((item) => item.type === 'dir') - const subDirectoryContents = await Promise.all( - subDirectories.map(async (dir) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const subPath = `${path}/${dir.name}` - return await fetchGitHubFilesRecursively(repository, subPath, ref) - }) - ) - - return fileContents - .filter((content) => content !== null) - .concat(...subDirectoryContents) - } else { + if (!Array.isArray(response.data)) { throw new Error('Failed to fetch directory.') } + + const files = response.data.filter((item) => item.type === 'file') + const fileContents = await Promise.all( + files.map(async (file): Promise => { + const fileResponse = await axios.get(file.download_url) + return { + path, + fileName: file.name, + content: fileResponse.data + } + }) + ) + + const subDirectories = response.data.filter((item) => item.type === 'dir') + const subDirectoryContents = await Promise.all( + subDirectories.map(async (dir) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const subPath = `${path}/${dir.name}` + return await fetchGitHubFilesRecursively(repository, subPath, ref) + }) + ) + + return fileContents + .filter((content) => content !== null) + .concat(...subDirectoryContents) } catch (error) { throw new Error('Error fetching directory') }