diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 04e61f59..291e5fd5 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -9,7 +9,10 @@ on: - "package.json" - "package-lock.json" - "vite.config.ts" +<<<<<<< HEAD +======= - "vitest.config.ts" +>>>>>>> main - "tsconfig*.json" - "eslint.config.js" @@ -33,9 +36,19 @@ jobs: - name: Type check run: npm run typecheck + - name: Type check + run: npm run typecheck + - name: Lint run: npm run lint +<<<<<<< HEAD + - name: i18n scan + run: npm run i18n:scan + + - name: Test + run: npm run test:frontend +======= - name: Test with coverage run: npm run test:coverage @@ -46,6 +59,7 @@ jobs: files: ./coverage/lcov.info flags: frontend fail_ci_if_error: false +>>>>>>> main - name: Build run: npm run build diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index 06974bec..44fb628e 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -49,6 +49,11 @@ jobs: run: npm run migrate working-directory: server +<<<<<<< HEAD + - name: Run tests + run: npm test + working-directory: server +======= - name: Verify migrations (apply, idempotency, rollback) run: npm run migrate:verify working-directory: server @@ -64,3 +69,4 @@ jobs: files: ./server/coverage/lcov.info flags: backend fail_ci_if_error: false +>>>>>>> main diff --git a/Cargo.toml b/Cargo.toml index be878de2..59185d34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,4 @@ strip = true [profile.release-with-logs] debug-assertions = true -inherits = "release" +inherits = "release" \ No newline at end of file diff --git a/contracts/course_milestone/src/lib.rs b/contracts/course_milestone/src/lib.rs index 7c63e2e9..85846db4 100644 --- a/contracts/course_milestone/src/lib.rs +++ b/contracts/course_milestone/src/lib.rs @@ -6,6 +6,17 @@ use soroban_sdk::{ panic_with_error, symbol_short, }; +<<<<<<< HEAD +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year +======= /// A single entry in a batch verification call. #[derive(Clone, Debug, Eq, PartialEq)] #[contracttype] @@ -28,6 +39,7 @@ const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; +>>>>>>> main #[contracttype] pub enum DataKey { @@ -39,7 +51,10 @@ pub enum DataKey { EnrolledCourses(Address), Course(String), CourseIds, +<<<<<<< HEAD +======= CompletedCount(Address, String), +>>>>>>> main } #[derive(Clone, Debug, Eq, PartialEq)] @@ -82,7 +97,13 @@ pub struct EnrolledEventData { const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); const LEARN_TOKEN_KEY: Symbol = symbol_short!("LRN_TKN"); +<<<<<<< HEAD +const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); // ✅ NEW +const PERSISTENT_TTL_THRESHOLD: u32 = 100; +const PERSISTENT_TTL_BUMP: u32 = 1_000; +======= const PAUSED_KEY: Symbol = symbol_short!("PAUSED"); +>>>>>>> main #[contracterror] #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -96,6 +117,11 @@ pub enum Error { CourseAlreadyComplete = 6, InvalidMilestones = 7, CourseAlreadyExists = 8, +<<<<<<< HEAD + AlreadyEnrolled = 9, + NotEnrolled = 10, + DuplicateSubmission = 11, +======= NotEnrolled = 9, DuplicateSubmission = 10, ContractPaused = 11, @@ -103,6 +129,7 @@ pub enum Error { InvalidState = 13, AlreadyCompleted = 14, InvalidReward = 15, +>>>>>>> main } #[derive(Clone, Debug, Eq, PartialEq)] @@ -134,18 +161,101 @@ pub struct CourseMilestone; #[contractimpl] impl CourseMilestone { - pub fn initialize(env: Env, admin: Address, learn_token_contract: Address) { + pub fn initialize(env: Env, admin: Address) { if env.storage().instance().has(&ADMIN_KEY) { panic_with_error!(&env, Error::AlreadyInitialized); } admin.require_auth(); env.storage().instance().set(&ADMIN_KEY, &admin); +<<<<<<< HEAD + env.storage() + .instance() + .set(&LEARN_TOKEN_KEY, &learn_token_contract); + } + + // Design decision: only the initialized admin can create course records. + // Design decision: course IDs are unique forever and removed courses stay on-chain as inactive records. + // Design decision: milestone_count must be > 0 so course configuration cannot represent an empty track. + pub fn add_course(env: Env, admin: Address, course_id: String, milestone_count: u32) { + Self::require_initialized(&env); + Self::require_admin(&env, &admin); + + if milestone_count == 0 { + panic_with_error!(&env, Error::InvalidMilestones); + } + + let course_key = DataKey::Course(course_id.clone()); + if env.storage().persistent().has(&course_key) { + panic_with_error!(&env, Error::CourseAlreadyExists); + } + + let config = CourseConfig { + milestone_count, + active: true, + }; + env.storage().persistent().set(&course_key, &config); + + let mut course_ids: Vec = env + .storage() + .persistent() + .get(&DataKey::CourseIds) + .unwrap_or_else(|| Vec::new(&env)); + course_ids.push_back(course_id); + env.storage() + .persistent() + .set(&DataKey::CourseIds, &course_ids); + } + + // Design decision: removed courses are marked inactive instead of deleted so historical references remain valid. + pub fn remove_course(env: Env, admin: Address, course_id: String) { + Self::require_initialized(&env); + Self::require_admin(&env, &admin); + + let course_key = DataKey::Course(course_id); + let mut config: CourseConfig = env + .storage() + .persistent() + .get(&course_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::CourseNotFound)); + config.active = false; + env.storage().persistent().set(&course_key, &config); + } + + pub fn get_course(env: Env, course_id: String) -> Option { + let course_key = DataKey::Course(course_id); + env.storage().persistent().get(&course_key) + } + + pub fn list_courses(env: Env) -> Vec { + let course_ids: Vec = env + .storage() + .persistent() + .get(&DataKey::CourseIds) + .unwrap_or_else(|| Vec::new(&env)); + + let mut active_courses = Vec::new(&env); + let mut i = 0; + while i < course_ids.len() { + let course_id = course_ids.get(i).unwrap(); + let course_key = DataKey::Course(course_id.clone()); + let config: Option = env.storage().persistent().get(&course_key); + if let Some(current) = config { + if current.active { + active_courses.push_back(course_id); + } + } + i += 1; + } + + active_courses +======= upgrade::init(&env); env.storage() .instance() .set(&LEARN_TOKEN_KEY, &learn_token_contract); Self::extend_instance(&env); +>>>>>>> main } pub fn add_course(env: Env, admin: Address, course_id: String, milestone_count: u32) { @@ -276,11 +386,23 @@ impl CourseMilestone { } } +<<<<<<< HEAD + fn assert_not_paused(env: &Env) { + if Self::is_paused(env.clone()) { + panic!("Contract is paused"); + } + +======= +>>>>>>> main pub fn enroll(env: Env, learner: Address, course_id: String) { Self::assert_not_paused(&env); Self::require_initialized(&env); learner.require_auth(); +<<<<<<< HEAD + // Enrollment is only allowed for registered, active courses. +======= +>>>>>>> main if !Self::is_course_active(&env, &course_id) { panic_with_error!(&env, Error::CourseNotFound); } @@ -305,7 +427,15 @@ impl CourseMilestone { env.events().publish( (symbol_short!("enrolled"),), +<<<<<<< HEAD + SubmittedEventData { + learner, + course_id, + evidence_uri: String::from_str(&env, ""), + }, +======= EnrolledEventData { learner, course_id }, +>>>>>>> main ); } @@ -313,7 +443,11 @@ impl CourseMilestone { let key = DataKey::Enrollment(learner, course_id); let enrolled = env.storage().persistent().get(&key).unwrap_or(false); if enrolled { +<<<<<<< HEAD + Self::bump_persistent_ttl(&env, &key); +======= Self::extend_persistent(&env, &key); +>>>>>>> main } enrolled } @@ -325,7 +459,14 @@ impl CourseMilestone { milestone_id: u32, evidence_uri: String, ) { +<<<<<<< HEAD + if Self::is_paused(env.clone()) { + panic_with_error!(&env, Error::ContractPaused); + } + +======= Self::assert_not_paused(&env); +>>>>>>> main Self::require_initialized(&env); learner.require_auth(); @@ -339,8 +480,13 @@ impl CourseMilestone { .persistent() .get::<_, MilestoneStatus>(&state_key) .unwrap_or(MilestoneStatus::NotStarted); + Self::bump_persistent_ttl(&env, &state_key); +<<<<<<< HEAD + if current_state != MilestoneStatus::NotStarted { +======= if current_state == MilestoneStatus::Pending || current_state == MilestoneStatus::Approved { +>>>>>>> main panic_with_error!(&env, Error::DuplicateSubmission); } @@ -353,6 +499,7 @@ impl CourseMilestone { DataKey::MilestoneSubmission(learner.clone(), course_id.clone(), milestone_id); env.storage().persistent().set(&submission_key, &submission); + Self::bump_persistent_ttl(&env, &submission_key); env.storage() .persistent() .set(&state_key, &MilestoneStatus::Pending); @@ -395,7 +542,11 @@ impl CourseMilestone { let key = DataKey::MilestoneSubmission(learner, course_id, milestone_id); let submission: Option = env.storage().persistent().get(&key); if submission.is_some() { +<<<<<<< HEAD + Self::bump_persistent_ttl(&env, &key); +======= Self::extend_persistent(&env, &key); +>>>>>>> main } submission } @@ -407,6 +558,12 @@ impl CourseMilestone { .persistent() .get(&key) .unwrap_or_else(|| Vec::new(&env)); +<<<<<<< HEAD + if courses.len() > 0 { + Self::bump_persistent_ttl(&env, &key); + } + courses +======= if !courses.is_empty() { Self::extend_persistent(&env, &key); } @@ -499,6 +656,7 @@ impl CourseMilestone { let admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); admin.require_auth(); upgrade::apply(&env, &admin, &new_wasm_hash); +>>>>>>> main } pub fn get_version(env: Env) -> String { @@ -520,15 +678,27 @@ impl CourseMilestone { Self::require_initialized(&env); admin.require_auth(); +<<<<<<< HEAD + // Verify admin authorization +======= +>>>>>>> main let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); if admin != stored_admin { panic_with_error!(&env, Error::Unauthorized); } +<<<<<<< HEAD + // Check if learner is enrolled +======= +>>>>>>> main if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { panic_with_error!(&env, Error::NotEnrolled); } +<<<<<<< HEAD + // Check current milestone state +======= +>>>>>>> main let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); let current_state = env .storage() @@ -540,16 +710,37 @@ impl CourseMilestone { panic_with_error!(&env, Error::InvalidState); } +<<<<<<< HEAD + // Update milestone state to Approved + env.storage() + .persistent() + .set(&state_key, &MilestoneStatus::Approved); + + // Get learn token contract address and mint tokens +======= env.storage() .persistent() .set(&state_key, &MilestoneStatus::Approved); let completed_key = DataKey::Completed(learner.clone(), course_id.clone(), milestone_id); env.storage().persistent().set(&completed_key, &true); +>>>>>>> main let learn_token_address: Address = env.storage().instance().get(&LEARN_TOKEN_KEY).unwrap(); let learn_token_client = LearnTokenClient::new(&env, &learn_token_address); learn_token_client.mint(&learner, &tokens_amount); +<<<<<<< HEAD + // Emit milestone completed event + env.events().publish( + symbol_short!("milestone_completed"), + MilestoneCompleted { + learner: learner.clone(), + course_id: course_id.clone().parse::().unwrap_or(0), + milestones_completed: milestone_id, + tokens_minted: tokens_amount, + }, + ); +======= Self::extend_persistent(&env, &state_key); Self::extend_persistent(&env, &completed_key); @@ -651,6 +842,7 @@ impl CourseMilestone { i += 1; } +>>>>>>> main } pub fn reject_milestone( @@ -667,19 +859,40 @@ impl CourseMilestone { Self::require_initialized(&env); admin.require_auth(); +<<<<<<< HEAD + // Verify admin authorization +======= +>>>>>>> main let stored_admin: Address = env.storage().instance().get(&ADMIN_KEY).unwrap(); if admin != stored_admin { panic_with_error!(&env, Error::Unauthorized); } +<<<<<<< HEAD + // Check if learner is enrolled +======= +>>>>>>> main if !Self::is_enrolled(env.clone(), learner.clone(), course_id.clone()) { panic_with_error!(&env, Error::NotEnrolled); } +<<<<<<< HEAD + // Check current milestone state +======= +>>>>>>> main let state_key = DataKey::MilestoneState(learner.clone(), course_id.clone(), milestone_id); let current_state = env .storage() .persistent() +<<<<<<< HEAD + .get::<_, CourseConfig>(&course_key) + { + Some(config) => { + Self::extend_persistent(env, &course_key); + config.active + }, + None => false, +======= .get::<_, MilestoneStatus>(&state_key) .unwrap_or(MilestoneStatus::NotStarted); @@ -698,9 +911,12 @@ impl CourseMilestone { fn require_initialized(env: &Env) { if !env.storage().instance().has(&ADMIN_KEY) { panic_with_error!(env, Error::NotInitialized); +>>>>>>> main } } +<<<<<<< HEAD +======= fn require_admin(env: &Env, admin: &Address) { admin.require_auth(); let stored_admin: Address = env @@ -772,5 +988,6 @@ impl CourseMilestone { } } +>>>>>>> main #[cfg(test)] mod test; diff --git a/contracts/course_milestone/src/test.rs b/contracts/course_milestone/src/test.rs index 5f7e9ae8..a216f4f6 100644 --- a/contracts/course_milestone/src/test.rs +++ b/contracts/course_milestone/src/test.rs @@ -1,6 +1,13 @@ extern crate std; use soroban_sdk::{ +<<<<<<< HEAD + Address, Env, String, + testutils::{Address as _, Ledger, LedgerInfo}, +}; + +use crate::{CourseConfig, CourseMilestone, CourseMilestoneClient, DataKey, Error, MilestoneStatus}; +======= Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec, contract, contractimpl, contracttype, symbol_short, testutils::{Address as _, Events as _, MockAuth, MockAuthInvoke}, @@ -34,11 +41,19 @@ impl MockLearnToken { .unwrap_or(0_i128) } } +>>>>>>> main fn sid(env: &Env, value: &str) -> String { String::from_str(env, value) } +<<<<<<< HEAD +fn setup() -> (Env, Address, Address, Address, CourseMilestoneClient<'static>) { + let env = Env::default(); + let admin = Address::generate(&env); + let learn_token = Address::generate(&env); + let learn_token_address = Address::generate(&env); +======= fn authorize(env: &Env, address: &Address, contract: &Address, fn_name: &'static str, args: T) where T: IntoVal>, @@ -65,9 +80,14 @@ fn setup() -> ( let env = Env::default(); let admin = Address::generate(&env); let learn_token_id = env.register(MockLearnToken, ()); +>>>>>>> main let contract_id = env.register(CourseMilestone, ()); let client = CourseMilestoneClient::new(&env, &contract_id); +<<<<<<< HEAD + client.initialize(&admin, &learn_token_contract); + (env, contract_id, admin, client) +======= let token_client = MockLearnTokenClient::new(&env, &learn_token_id); authorize( @@ -146,6 +166,24 @@ fn submit_milestone( ), ); client.submit_milestone(learner, course_id, &milestone_id, evidence_uri); +>>>>>>> main +} + +// ======================= +// ✅ ENROLL TESTS +// ======================= + +fn set_ledger_sequence(env: &Env, sequence_number: u32) { + env.ledger().set(LedgerInfo { + timestamp: 1_700_000_000, + protocol_version: 23, + sequence_number, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 16, + min_persistent_entry_ttl: 16, + max_entry_ttl: 6312000, + }); } #[test] @@ -173,20 +211,75 @@ fn enrolls_learner_in_active_course() { let learner = Address::generate(&env); let course_id = sid(&env, "rust-101"); +<<<<<<< HEAD + client.add_course(&admin, &course_id, &10); + client.enroll(&learner, &course_id); +======= add_course(&env, &contract_id, &admin, &client, &course_id, 3); enroll(&env, &contract_id, &learner, &client, &course_id); +>>>>>>> main assert!(client.is_enrolled(&learner, &course_id)); } #[test] fn duplicate_enroll_fails() { +<<<<<<< HEAD + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + client.enroll(&learner, &course_id); + + let result = client.try_enroll(&learner, &course_id); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::Unauthorized as u32 + ))) + ); +} + +#[test] +fn enroll_fails_when_not_initialized() { + let env = Env::default(); + let admin = Address::generate(&env); + let learn_token_address = Address::generate(&env); + let contract_id = env.register(CourseMilestone, ()); + let client = CourseMilestoneClient::new(&env, &contract_id); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + let result = client.try_enroll(&learner, &course_id); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NotInitialized as u32 + ))) + ); +} + +// ======================= +// ✅ SUBMIT MILESTONE TESTS +// ======================= + +#[test] +fn enrolled_learner_can_submit_once_and_submission_is_stored() { + let (env, _contract_id, _admin, client) = setup(); +======= let (env, contract_id, admin, _token_id, client, _token_client) = setup(); +>>>>>>> main let learner = Address::generate(&env); let course_id = sid(&env, "rust-101"); +<<<<<<< HEAD + client.add_course(&admin, &course_id, &5); + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); +======= add_course(&env, &contract_id, &admin, &client, &course_id, 3); enroll(&env, &contract_id, &learner, &client, &course_id); +>>>>>>> main authorize( &env, @@ -236,8 +329,13 @@ fn submit_milestone_stores_pending_submission() { } #[test] +<<<<<<< HEAD +fn non_enrolled_learner_cannot_submit() { + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); +======= fn verify_milestone_mints_lrn_and_marks_completion() { let (env, contract_id, admin, _token_id, client, token_client) = setup(); +>>>>>>> main let learner = Address::generate(&env); let course_id = sid(&env, "rust-101"); let evidence_uri = sid(&env, "ipfs://proof"); @@ -624,8 +722,14 @@ fn complete_milestone_fails_without_admin_auth() { let attacker = Address::generate(&env); let course_id = sid(&env, "rust-101"); +<<<<<<< HEAD + client.add_course(&admin, &course_id, &8); + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &7, &evidence_uri); +======= add_course(&env, &contract_id, &admin, &client, &course_id, 3); enroll(&env, &contract_id, &learner, &client, &course_id); +>>>>>>> main authorize( &env, @@ -773,6 +877,147 @@ fn batch_verify_milestones_reverts_on_invalid_entry() { assert_eq!(token_client.balance(&learner1), 0); } +// ======================= +// ✅ VERIFY MILESTONE TESTS +// ======================= + +#[test] +fn verify_milestone_happy_path() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + + client.verify_milestone(&admin, &learner, &course_id, &1, &100); + + let status = client.get_milestone_status(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Approved); +} + +#[test] +fn verify_milestone_fails_for_non_admin() { + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let non_admin = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + + let result = client.try_verify_milestone(&non_admin, &learner, &course_id, &1, &100); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::Unauthorized as u32 + ))) + ); +} + +#[test] +fn verify_milestone_fails_for_already_verified() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + client.verify_milestone(&admin, &learner, &course_id, &1, &100); + + let result = client.try_verify_milestone(&admin, &learner, &course_id, &1, &100); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidState as u32 + ))) + ); +} + +#[test] +fn verify_milestone_fails_for_not_enrolled_learner() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + let result = client.try_verify_milestone(&admin, &learner, &course_id, &1, &100); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::NotEnrolled as u32 + ))) + ); +} + +// ======================= +// ✅ REJECT MILESTONE TESTS +// ======================= + +#[test] +fn reject_milestone_happy_path() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + + client.reject_milestone(&admin, &learner, &course_id, &1); + + let status = client.get_milestone_status(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Rejected); + + // Submission should be removed + let submission = client.get_milestone_submission(&learner, &course_id, &1); + assert!(submission.is_none()); +} + +#[test] +fn reject_milestone_fails_for_non_admin() { + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let non_admin = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + + let result = client.try_reject_milestone(&non_admin, &learner, &course_id, &1); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::Unauthorized as u32 + ))) + ); +} + +#[test] +fn reject_milestone_fails_for_wrong_state() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + client.enroll(&learner, &course_id); + + // Try to reject a milestone that hasn't been submitted + let result = client.try_reject_milestone(&admin, &learner, &course_id, &1); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::InvalidState as u32 + ))) + ); +} + +// ======================= +// ✅ GET MILESTONE STATUS TESTS +// ======================= + #[test] fn upgrade_requires_admin_auth() { let (env, contract_id, _admin, _token_id, client, _token_client) = setup(); @@ -797,6 +1042,221 @@ fn state_persists_after_upgrade() { let learner = Address::generate(&env); let course_id = sid(&env, "soroban-101"); +<<<<<<< HEAD + let status = client.get_milestone_state(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::NotStarted); +} + +#[test] +fn get_milestone_status_returns_pending_after_submission() { + let (env, _contract_id, admin, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence = sid(&env, "ipfs://bafy-proof"); + + client.add_course(&admin, &course_id, &4); + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence); + + let status = client.get_milestone_state(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Pending); +} + +#[test] +fn get_milestone_status_returns_approved_after_verification() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence); + client.verify_milestone(&admin, &learner, &course_id, &1, &100); + + let status = client.get_milestone_status(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Approved); +} + +#[test] +fn get_milestone_status_returns_rejected_after_rejection() { + let (env, _contract_id, admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence); + client.reject_milestone(&admin, &learner, &course_id, &1); + + let status = client.get_milestone_status(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Rejected); +} + +#[test] +fn get_milestone_status_not_started_for_unsubmitted_milestone() { + let (env, _contract_id, admin, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence = sid(&env, "ipfs://bafy-proof"); + + client.add_course(&admin, &course_id, &4); + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence); + + let status = client.get_milestone_state(&learner, &course_id, &2); + assert_eq!(status, MilestoneStatus::NotStarted); +} + +// ======================= +// ✅ LRN MINTING INTEGRATION TESTS +// ======================= + +#[test] +fn verify_milestone_mints_lrn_tokens() { + let (env, _contract_id, admin, learn_token_address, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + let evidence_uri = sid(&env, "ipfs://bafy-proof"); + + client.enroll(&learner, &course_id); + client.submit_milestone(&learner, &course_id, &1, &evidence_uri); + + // This would require a mock learn token contract for full testing + // For now, we just verify the function call succeeds + client.verify_milestone(&admin, &learner, &course_id, &1, &100); + + let status = client.get_milestone_status(&learner, &course_id, &1); + assert_eq!(status, MilestoneStatus::Approved); +} + +// ======================= +// ✅ ENROLLED COURSES TESTS +// ======================= + +#[test] +fn get_enrolled_courses_returns_empty_for_new_learner() { + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); + let learner = Address::generate(&env); + + let courses = client.get_enrolled_courses(&learner); + assert_eq!(courses.len(), 0); +} + +#[test] +fn get_enrolled_courses_returns_enrolled_courses() { + let (env, _contract_id, _admin, client) = setup(); + let learner = Address::generate(&env); + + client.add_course(&admin, &sid(&env, "rust-101"), &3); + client.add_course(&admin, &sid(&env, "defi-201"), &6); + client.enroll(&learner, &sid(&env, "rust-101")); + client.enroll(&learner, &sid(&env, "defi-201")); + + let courses = client.get_enrolled_courses(&learner); + assert_eq!(courses.len(), 2); + assert_eq!(courses.get(0).unwrap(), sid(&env, "rust-101")); + assert_eq!(courses.get(1).unwrap(), sid(&env, "defi-201")); +} + +#[test] +fn get_enrolled_courses_is_per_learner() { + let (env, _contract_id, _admin, client) = setup(); + let learner_a = Address::generate(&env); + let learner_b = Address::generate(&env); + + client.add_course(&admin, &sid(&env, "rust-101"), &3); + client.add_course(&admin, &sid(&env, "defi-201"), &6); + client.enroll(&learner_a, &sid(&env, "rust-101")); + client.enroll(&learner_a, &sid(&env, "defi-201")); + client.enroll(&learner_b, &sid(&env, "rust-101")); + + assert_eq!(client.get_enrolled_courses(&learner_a).len(), 2); + assert_eq!(client.get_enrolled_courses(&learner_b).len(), 1); +} + +// ======================= +// ✅ VERSION TESTS +// ======================= + +#[test] +fn get_version_returns_semver() { + let (env, _contract_id, _admin, _learn_token_address, client) = setup(); + assert_eq!(client.get_version(), String::from_str(&env, "1.0.0")); +} + +#[test] +fn add_course_and_get_course_work() { + let (env, _contract_id, admin, client) = setup(); + let course_id = sid(&env, "soroban-101"); + + client.add_course(&admin, &course_id, &12); + + let course = client + .get_course(&course_id) + .expect("course should be stored after add"); + assert_eq!( + course, + CourseConfig { + milestone_count: 12, + active: true, + } + ); +} + +#[test] +fn list_courses_returns_empty_when_none_exist() { + let (_env, _contract_id, _admin, client) = setup(); + assert_eq!(client.list_courses().len(), 0); +} + +#[test] +fn list_courses_returns_only_active_courses() { + let (env, _contract_id, admin, client) = setup(); + let course_a = sid(&env, "rust-101"); + let course_b = sid(&env, "defi-201"); + + client.add_course(&admin, &course_a, &5); + client.add_course(&admin, &course_b, &7); + client.remove_course(&admin, &course_b); + + let courses = client.list_courses(); + assert_eq!(courses.len(), 1); + assert_eq!(courses.get(0).unwrap(), course_a); +} + +#[test] +fn remove_course_marks_course_inactive() { + let (env, _contract_id, admin, client) = setup(); + let course_id = sid(&env, "rust-101"); + let learner = Address::generate(&env); + + client.add_course(&admin, &course_id, &4); + client.remove_course(&admin, &course_id); + + let stored = client + .get_course(&course_id) + .expect("course should remain stored"); + assert_eq!(stored.active, false); + + let result = client.try_enroll(&learner, &course_id); + assert_eq!( + result.err(), + Some(Ok(soroban_sdk::Error::from_contract_error( + Error::CourseNotFound as u32 + ))) + ); +} + +#[test] +fn pause_blocks_enroll() { + let (env, _contract_id, admin, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + client.pause(&admin); + + let result = client.try_enroll(&learner, &course_id); +======= add_course(&env, &contract_id, &admin, &client, &course_id, 3); enroll(&env, &contract_id, &learner, &client, &course_id); @@ -816,6 +1276,7 @@ fn state_persists_after_upgrade() { .unwrap_or(false) }); let stored_hash = env.as_contract(&contract_id, || crate::upgrade::current_hash(&env)); +>>>>>>> main assert_eq!( stored_course, @@ -834,11 +1295,17 @@ fn benchmark_costs() { let learner = Address::generate(&env); let course_id = sid(&env, "rust-101"); +<<<<<<< HEAD + client.add_course(&admin, &course_id, &1); + client.enroll(&learner, &course_id); + client.pause(&admin); +======= // 1. Benchmark add_course env.cost_estimate().budget().reset_unlimited(); add_course(&env, &contract_id, &admin, &client, &course_id, 3); let add_instr = env.cost_estimate().budget().cpu_instruction_cost(); let add_mem = env.cost_estimate().budget().memory_bytes_cost(); +>>>>>>> main // 2. Benchmark enroll env.cost_estimate().budget().reset_unlimited(); @@ -865,3 +1332,21 @@ fn benchmark_costs() { std::println!("enroll: instr={}, mem={}", enroll_instr, enroll_mem); std::println!("complete_milestone: instr={}, mem={}", comp_instr, comp_mem); } +<<<<<<< HEAD + +#[test] +fn unpause_restores_functionality() { + let (env, _contract_id, admin, client) = setup(); + let learner = Address::generate(&env); + let course_id = sid(&env, "rust-101"); + + client.add_course(&admin, &course_id, &1); + client.pause(&admin); + client.unpause(&admin); + + client.enroll(&learner, &course_id); + + assert!(client.is_enrolled(&learner, &course_id)); +} +======= +>>>>>>> main diff --git a/contracts/fungible-allowlist/src/lib.rs b/contracts/fungible-allowlist/src/lib.rs index 81987c47..19b3d9d1 100644 --- a/contracts/fungible-allowlist/src/lib.rs +++ b/contracts/fungible-allowlist/src/lib.rs @@ -1,7 +1,13 @@ +<<<<<<< HEAD +use soroban_sdk::{ + Address, Env, Vec, contract, contracterror, contractimpl, contracttype, panic_with_error, + symbol_short, +======= #![no_std] use soroban_sdk::{ Address, Env, Vec, contract, contracterror, contractimpl, contracttype, panic_with_error, +>>>>>>> main }; #[contracterror] @@ -17,20 +23,44 @@ pub enum AllowlistError { pub enum DataKey { Admin, IsAllowed(Address), +<<<<<<< HEAD + Allowlist, +} + +#[contract] +// Placeholder — implementation pending. + +use soroban_sdk::{contract, contractimpl}; + +#[contract] +======= } #[contract] +>>>>>>> main pub struct FungibleAllowlist; #[contractimpl] impl FungibleAllowlist { +<<<<<<< HEAD + /// Initialize the contract with an administrator. +======= +>>>>>>> main pub fn initialize(env: Env, admin: Address) { if env.storage().instance().has(&DataKey::Admin) { panic_with_error!(&env, AllowlistError::AlreadyInitialized); } env.storage().instance().set(&DataKey::Admin, &admin); +<<<<<<< HEAD + let empty_list: Vec
= Vec::new(&env); + env.storage().instance().set(&DataKey::Allowlist, &empty_list); + } + + /// Add an account to the allowlist. Only the administrator can call this. +======= } +>>>>>>> main pub fn add_to_allowlist(env: Env, admin: Address, account: Address) { admin.require_auth(); let stored_admin: Address = env @@ -43,12 +73,23 @@ impl FungibleAllowlist { } if !Self::is_allowed(env.clone(), account.clone()) { +<<<<<<< HEAD + env.storage().persistent().set(&DataKey::IsAllowed(account.clone()), &true); + let mut list: Vec
= env.storage().instance().get(&DataKey::Allowlist).unwrap(); + list.push_back(account); + env.storage().instance().set(&DataKey::Allowlist, &list); + } + } + + /// Remove an account from the allowlist. Only the administrator can call this. +======= env.storage() .persistent() .set(&DataKey::IsAllowed(account.clone()), &true); } } +>>>>>>> main pub fn remove_from_allowlist(env: Env, admin: Address, account: Address) { admin.require_auth(); let stored_admin: Address = env @@ -61,6 +102,21 @@ impl FungibleAllowlist { } if Self::is_allowed(env.clone(), account.clone()) { +<<<<<<< HEAD + env.storage().persistent().set(&DataKey::IsAllowed(account.clone()), &false); + let list: Vec
= env.storage().instance().get(&DataKey::Allowlist).unwrap(); + let mut new_list: Vec
= Vec::new(&env); + for x in list.iter() { + if x != account { + new_list.push_back(x); + } + } + env.storage().instance().set(&DataKey::Allowlist, &new_list); + } + } + + /// Returns true if the account is in the allowlist. +======= env.storage() .persistent() .set(&DataKey::IsAllowed(account.clone()), &false); @@ -72,6 +128,7 @@ impl FungibleAllowlist { } } +>>>>>>> main pub fn is_allowed(env: Env, account: Address) -> bool { env.storage() .persistent() @@ -79,11 +136,23 @@ impl FungibleAllowlist { .unwrap_or(false) } +<<<<<<< HEAD + /// Returns the complete list of allowed accounts. + pub fn get_allowlist(env: Env) -> Vec
{ + env.storage() + .instance() + .get(&DataKey::Allowlist) + .unwrap_or_else(|| Vec::new(&env)) + } + + /// Transfer administrative role to a new address. +======= pub fn get_allowlist(env: Env) -> Vec
{ // Enumeration should be rebuilt off-chain from events or indexers. Vec::new(&env) } +>>>>>>> main pub fn set_admin(env: Env, admin: Address, new_admin: Address) { admin.require_auth(); let stored_admin: Address = env @@ -101,7 +170,11 @@ impl FungibleAllowlist { #[cfg(test)] mod test { use super::*; +<<<<<<< HEAD + use soroban_sdk::{testutils::Address as _, Env}; +======= use soroban_sdk::{Env, testutils::Address as _}; +>>>>>>> main #[test] fn test_allowlist_flow() { @@ -117,6 +190,36 @@ mod test { assert_eq!(client.is_allowed(&alice), false); assert_eq!(client.get_allowlist().len(), 0); +<<<<<<< HEAD + // Add Alice + env.mock_all_auths(); + client.add_to_allowlist(&admin, &alice); + assert_eq!(client.is_allowed(&alice), true); + assert_eq!(client.get_allowlist().len(), 1); + assert_eq!(client.get_allowlist().get(0).unwrap(), alice); + + // Add Bob + client.add_to_allowlist(&admin, &bob); + assert_eq!(client.is_allowed(&bob), true); + assert_eq!(client.get_allowlist().len(), 2); + + // Remove Alice + client.remove_from_allowlist(&admin, &alice); + assert_eq!(client.is_allowed(&alice), false); + assert_eq!(client.get_allowlist().len(), 1); + assert_eq!(client.get_allowlist().get(0).unwrap(), bob); + + // Set Admin + let new_admin = Address::generate(&env); + client.set_admin(&admin, &new_admin); + + // Try to add with old admin (should fail due to unauthorized) + // Wait, mock_all_auths is on, so we should test real auth maybe? + // But for unit test, we can just verify it works with new admin. + client.add_to_allowlist(&new_admin, &alice); + assert_eq!(client.is_allowed(&alice), true); + } +======= env.mock_all_auths(); client.add_to_allowlist(&admin, &alice); @@ -165,4 +268,5 @@ mod test { std::println!("initialize: instr={}, mem={}", init_instr, init_mem); std::println!("add_to_allowlist: instr={}, mem={}", add_instr, add_mem); } +>>>>>>> main } diff --git a/contracts/governance_token/src/lib.rs b/contracts/governance_token/src/lib.rs index 818bdaf4..0c909a1b 100644 --- a/contracts/governance_token/src/lib.rs +++ b/contracts/governance_token/src/lib.rs @@ -14,8 +14,13 @@ //! Implements: https://github.com/bakeronchain/learnvault/issues/11 use soroban_sdk::{ +<<<<<<< HEAD + Address, Env, String, Symbol, contract, contracterror, contractevent, contractimpl, contracttype, + panic_with_error, symbol_short, +======= Address, BytesN, Env, String, Symbol, contract, contracterror, contractevent, contractimpl, contracttype, panic_with_error, symbol_short, +>>>>>>> main }; use learnvault_shared::upgrade; @@ -32,6 +37,18 @@ const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year +const TEMP_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const TEMP_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year + // --------------------------------------------------------------------------- // Errors // --------------------------------------------------------------------------- @@ -81,6 +98,8 @@ pub struct GOVBurned { pub amount: i128, } +<<<<<<< HEAD +======= #[contractevent] #[derive(Clone, Debug, Eq, PartialEq)] pub struct GOVPaused { @@ -116,6 +135,7 @@ pub struct GOVApproved { pub amount: i128, } +>>>>>>> main // --------------------------------------------------------------------------- // Contract // --------------------------------------------------------------------------- @@ -139,9 +159,13 @@ impl GovernanceToken { .set(&NAME_KEY, &String::from_str(&env, "LearnVault Governance")); env.storage() .instance() - .set(&SYMBOL_KEY, &String::from_str(&env, "GOV")); + .set(&SYMBOL_KEY, &symbol_short!("GOV")); env.storage().instance().set(&DECIMALS_KEY, &7_u32); +<<<<<<< HEAD + +======= +>>>>>>> main Self::extend_instance(&env); } @@ -151,7 +175,10 @@ impl GovernanceToken { /// Mint `amount` GOV to `to`. Admin only. pub fn mint(env: Env, to: Address, amount: i128) { +<<<<<<< HEAD +======= Self::assert_not_paused(&env); +>>>>>>> main Self::extend_instance(&env); let admin: Address = env .storage() @@ -237,6 +264,52 @@ impl GovernanceToken { GOVBurned { from, amount }.publish(&env); } + /// Burn `amount` from the caller's own balance. + pub fn burn(env: Env, from: Address, amount: i128) { + Self::extend_instance(&env); + from.require_auth(); + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + Self::_debit(&env, &from, amount); + // reduce total supply + let supply: i128 = env + .storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSupply, &(supply - amount)); + GOVBurned { from, amount }.publish(&env); + } + + /// Administrative burn for slashing. + pub fn admin_burn_from(env: Env, from: Address, amount: i128) { + Self::extend_instance(&env); + let admin: Address = env + .storage() + .instance() + .get(&ADMIN_KEY) + .unwrap_or_else(|| panic_with_error!(&env, GOVError::NotInitialized)); + admin.require_auth(); + + if amount <= 0 { + panic_with_error!(&env, GOVError::ZeroAmount); + } + Self::_debit(&env, &from, amount); + + let supply: i128 = env + .storage() + .instance() + .get(&DataKey::TotalSupply) + .unwrap_or(0); + env.storage() + .instance() + .set(&DataKey::TotalSupply, &(supply - amount)); + GOVBurned { from, amount }.publish(&env); + } + /// Transfer the admin role to a new address. pub fn set_admin(env: Env, new_admin: Address) { Self::extend_instance(&env); @@ -310,7 +383,10 @@ impl GovernanceToken { /// Transfer `amount` GOV from `from` to `to`. Requires `from` auth. pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { +<<<<<<< HEAD +======= Self::assert_not_paused(&env); +>>>>>>> main Self::extend_instance(&env); from.require_auth(); if amount <= 0 { @@ -332,6 +408,13 @@ impl GovernanceToken { ) { Self::assert_not_paused(&env); owner.require_auth(); +<<<<<<< HEAD + let key = DataKey::Allowance(owner, spender); + env.storage() + .temporary() + .set(&key, &amount); + env.storage().temporary().extend_ttl(&key, TEMP_BUMP_THRESHOLD, TEMP_EXTEND_TO); +======= let current_ledger = env.ledger().sequence(); if expiration_ledger < current_ledger { panic_with_error!(&env, GOVError::InvalidExpiration); @@ -349,6 +432,7 @@ impl GovernanceToken { amount, } .publish(&env); +>>>>>>> main } /// Transfer `amount` from `from` to `to` using `spender`'s allowance. @@ -360,6 +444,16 @@ impl GovernanceToken { let current_ledger = env.ledger().sequence(); let allow_key = DataKey::Allowance(from.clone(), spender.clone()); +<<<<<<< HEAD + let allowance: i128 = env.storage().temporary().get(&allow_key).unwrap_or(0); + if allowance < amount { + panic_with_error!(&env, GOVError::InsufficientFunds); + } + env.storage() + .temporary() + .set(&allow_key, &(allowance - amount)); + env.storage().temporary().extend_ttl(&allow_key, TEMP_BUMP_THRESHOLD, TEMP_EXTEND_TO); +======= let (allowance, expiration_ledger): (i128, u32) = env.storage().persistent().get(&allow_key).unwrap_or((0, 0)); @@ -381,6 +475,7 @@ impl GovernanceToken { Self::extend_persistent(&env, &allow_key); } +>>>>>>> main Self::_debit(&env, &from, amount); Self::_credit(&env, &to, amount); @@ -472,6 +567,11 @@ impl GovernanceToken { pub fn allowance(env: Env, owner: Address, spender: Address) -> i128 { let key = DataKey::Allowance(owner, spender); +<<<<<<< HEAD + if let Some(allowance) = env.storage().temporary().get::<_, i128>(&key) { + env.storage().temporary().extend_ttl(&key, TEMP_BUMP_THRESHOLD, TEMP_EXTEND_TO); + allowance +======= if let Some((allowance, expiration_ledger)) = env.storage().persistent().get::<_, (i128, u32)>(&key) { @@ -481,6 +581,7 @@ impl GovernanceToken { Self::extend_persistent(&env, &key); allowance } +>>>>>>> main } else { 0 } @@ -556,6 +657,8 @@ impl GovernanceToken { } } +<<<<<<< HEAD +======= fn assert_not_paused(env: &Env) { let paused: bool = env.storage().instance().get(&PAUSED_KEY).unwrap_or(false); if paused { @@ -563,6 +666,7 @@ impl GovernanceToken { } } +>>>>>>> main fn extend_instance(env: &Env) { env.storage() .instance() @@ -1106,6 +1210,8 @@ mod test { assert_eq!(client.allowance(&alice, &bob), 0); } +<<<<<<< HEAD +======= #[test] fn approve_rejects_past_expiration() { let e = Env::default(); @@ -1167,6 +1273,7 @@ mod test { assert_eq!(client.allowance(&alice, &bob), 15); } +>>>>>>> main // --- burning --- #[test] @@ -1234,6 +1341,8 @@ mod test { client.burn(&alice, &40); assert_eq!(client.get_voting_power(&bob), 60); } +<<<<<<< HEAD +======= // --- pause / unpause --- @@ -1373,4 +1482,5 @@ mod test { std::println!("mint: instr={}, mem={}", mint_instr, mint_mem); std::println!("transfer: instr={}, mem={}", xfer_instr, xfer_mem); } +>>>>>>> main } diff --git a/contracts/learn_token/src/lib.rs b/contracts/learn_token/src/lib.rs index 9f834fa9..d1ead7c2 100644 --- a/contracts/learn_token/src/lib.rs +++ b/contracts/learn_token/src/lib.rs @@ -33,6 +33,16 @@ const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year + // --------------------------------------------------------------------------- // Errors // --------------------------------------------------------------------------- @@ -91,7 +101,11 @@ impl LearnToken { .instance() .set(&SYMBOL_KEY, &String::from_str(&env, "LRN")); env.storage().instance().set(&DECIMALS_KEY, &7_u32); +<<<<<<< HEAD + +======= +>>>>>>> main Self::extend_instance(&env); } @@ -131,6 +145,10 @@ impl LearnToken { .set(&DataKey::TotalSupply, &(supply + amount)); // Extend persistent storage for balance entries +<<<<<<< HEAD + env.storage().persistent().extend_ttl(&bal_key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); + env.storage().persistent().extend_ttl(&DataKey::TotalSupply, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); +======= env.storage().persistent().extend_ttl( &bal_key, PERSISTENT_BUMP_THRESHOLD, @@ -141,6 +159,7 @@ impl LearnToken { PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO, ); +>>>>>>> main // 5. Emit event env.events() @@ -207,11 +226,15 @@ impl LearnToken { Self::extend_instance(&env); let key = DataKey::Balance(account); if let Some(bal) = env.storage().persistent().get::<_, i128>(&key) { +<<<<<<< HEAD + env.storage().persistent().extend_ttl(&key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); +======= env.storage().persistent().extend_ttl( &key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO, ); +>>>>>>> main bal } else { 0 @@ -222,11 +245,15 @@ impl LearnToken { Self::extend_instance(&env); let key = DataKey::TotalSupply; if let Some(supply) = env.storage().persistent().get::<_, i128>(&key) { +<<<<<<< HEAD + env.storage().persistent().extend_ttl(&key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); +======= env.storage().persistent().extend_ttl( &key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO, ); +>>>>>>> main supply } else { 0 diff --git a/contracts/learn_token/src/test.rs b/contracts/learn_token/src/test.rs index 590a2021..8ee63d41 100644 --- a/contracts/learn_token/src/test.rs +++ b/contracts/learn_token/src/test.rs @@ -748,6 +748,8 @@ fn reputation_score_matches_balance_division() { ); } } +<<<<<<< HEAD +======= #[test] fn upgrade_requires_admin_auth() { @@ -841,3 +843,4 @@ fn benchmark_costs() { std::println!("mint: instr={}, mem={}", mint_instr, mint_mem); std::println!("reputation_score: instr={}, mem={}", rep_instr, rep_mem); } +>>>>>>> main diff --git a/contracts/milestone_escrow/src/lib.rs b/contracts/milestone_escrow/src/lib.rs index 45c4bdc5..2865ff32 100644 --- a/contracts/milestone_escrow/src/lib.rs +++ b/contracts/milestone_escrow/src/lib.rs @@ -5,6 +5,11 @@ use soroban_sdk::{ contracttype, panic_with_error, symbol_short, }; +<<<<<<< HEAD +const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); +const TREASURY_KEY: Symbol = symbol_short!("TREAS"); +const INACTIVITY_WINDOW_KEY: Symbol = symbol_short!("INACT_W"); +======= use learnvault_shared::upgrade; pub use upgrade::ContractUpgraded; @@ -18,6 +23,7 @@ pub struct Config { pub treasury: Address, pub inactivity_window: u64, } +>>>>>>> main #[derive(Clone)] #[contracttype] @@ -86,12 +92,30 @@ pub struct EscrowReclaimed { #[contractimpl] impl MilestoneEscrow { +<<<<<<< HEAD + pub fn initialize( + env: Env, + admin: Address, + treasury: Address, + inactivity_window_seconds: u64, + ) { + if env.storage().instance().has(&ADMIN_KEY) { +======= pub fn initialize(env: Env, admin: Address, treasury: Address, inactivity_window_seconds: u64) { if env.storage().instance().has(&CONFIG_KEY) { +>>>>>>> main panic_with_error!(&env, Error::AlreadyInitialized); } admin.require_auth(); +<<<<<<< HEAD + // Keep 30 days (30 * 24 * 60 * 60) as the recommended default at deployment. + env.storage().instance().set(&ADMIN_KEY, &admin); + env.storage().instance().set(&TREASURY_KEY, &treasury); + env.storage() + .instance() + .set(&INACTIVITY_WINDOW_KEY, &inactivity_window_seconds); +======= let config = Config { admin, treasury, @@ -99,6 +123,7 @@ impl MilestoneEscrow { }; env.storage().instance().set(&CONFIG_KEY, &config); upgrade::init(&env); +>>>>>>> main } pub fn create_escrow( @@ -179,8 +204,13 @@ impl MilestoneEscrow { let now = env.ledger().timestamp(); let inactive_for = now.saturating_sub(record.last_activity); +<<<<<<< HEAD + let inactivity_window = Self::inactivity_window(&env); + if inactive_for < inactivity_window { +======= let config = Self::get_config(&env); if inactive_for < config.inactivity_window { +>>>>>>> main panic_with_error!(&env, Error::InactivityNotReached); } @@ -198,7 +228,10 @@ impl MilestoneEscrow { record.released_amount = record.total_amount; record.last_activity = now; env.storage().persistent().set(&key, &record); +<<<<<<< HEAD +======= +>>>>>>> main EscrowReclaimed { proposal_id, scholar: record.scholar.clone(), @@ -246,6 +279,18 @@ impl MilestoneEscrow { .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)) } + fn inactivity_window(env: &Env) -> u64 { + if let Some(window) = env + .storage() + .instance() + .get::<_, u64>(&INACTIVITY_WINDOW_KEY) + { + window + } else { + panic_with_error!(env, Error::NotInitialized); + } + } + pub fn get_version(env: Env) -> String { String::from_str(&env, "1.0.0") } diff --git a/contracts/milestone_escrow/src/test.rs b/contracts/milestone_escrow/src/test.rs index 36aa6b4e..23517a13 100644 --- a/contracts/milestone_escrow/src/test.rs +++ b/contracts/milestone_escrow/src/test.rs @@ -269,6 +269,8 @@ fn reclaim_inactive_uses_configured_window_size() { } #[test] +<<<<<<< HEAD +======= fn reclaim_inactive_emits_event() { let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); let client = MilestoneEscrowClient::new(&env, &contract_id); @@ -288,6 +290,7 @@ fn reclaim_inactive_emits_event() { } #[test] +>>>>>>> main fn get_escrow_reflects_each_stage_of_the_full_flow() { let (env, contract_id, _token, _admin, _treasury, scholar) = setup(); let client = MilestoneEscrowClient::new(&env, &contract_id); diff --git a/contracts/scholar_nft/src/lib.rs b/contracts/scholar_nft/src/lib.rs index 7cb493ca..787738b6 100644 --- a/contracts/scholar_nft/src/lib.rs +++ b/contracts/scholar_nft/src/lib.rs @@ -2,9 +2,27 @@ #![allow(deprecated)] use soroban_sdk::{ +<<<<<<< HEAD + Address, Env, String, contract, contracterror, contractimpl, contracttype, panic_with_error, + symbol_short, +======= Address, BytesN, Env, String, Symbol, Vec, contract, contracterror, contractimpl, contracttype, panic_with_error, symbol_short, +>>>>>>> main }; + contract, contracterror, contractimpl, contracttype, panic_with_error, symbol_short, Address, + Env, String, Symbol, +}; + +// --------------------------------------------------------------------------- +// Storage Constants (assuming ~6s ledger time) +// --------------------------------------------------------------------------- + +const DAY_IN_LEDGERS: u32 = 17_280; +const INSTANCE_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; // 30 days +const PERSISTENT_BUMP_THRESHOLD: u32 = DAY_IN_LEDGERS; +const PERSISTENT_EXTEND_TO: u32 = DAY_IN_LEDGERS * 365; // 1 year use learnvault_shared::upgrade; @@ -16,6 +34,11 @@ const INSTANCE_EXTEND_TO: u32 = DAY_IN_LEDGERS * 30; const TTL_MIN: u32 = DAY_IN_LEDGERS; const TTL_MAX: u32 = DAY_IN_LEDGERS * 365; +<<<<<<< HEAD +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- +======= const ADMIN_KEY: Symbol = symbol_short!("ADMIN"); const TOKEN_COUNTER_KEY: Symbol = symbol_short!("TCOUNTER"); @@ -26,16 +49,23 @@ pub struct ScholarMetadata { pub metadata_uri: String, pub issued_at: u64, } +>>>>>>> main #[derive(Clone, Debug, Eq, PartialEq)] #[contracttype] pub enum DataKey { Admin, Counter, +<<<<<<< HEAD + Owner(u64), // token_id -> Address + TokenUri(u64), // token_id -> String + Revoked(u64), // token_id -> String (reason) +======= Owner(u64), TokenUri(u64), Revoked(u64), Metadata(u64), +>>>>>>> main } #[derive(Clone, Debug, Eq, PartialEq)] @@ -66,12 +96,18 @@ pub struct RevokedEventData { pub reason: String, } +<<<<<<< HEAD +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- +======= #[derive(Clone, Debug, Eq, PartialEq)] #[contracttype] pub struct AdminChangedEventData { pub old_admin: Address, pub new_admin: Address, } +>>>>>>> main #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -86,6 +122,13 @@ pub enum ScholarNFTError { Soulbound = 7, AlreadyRevoked = 8, } +<<<<<<< HEAD + +// --------------------------------------------------------------------------- +// Contract +// --------------------------------------------------------------------------- +======= +>>>>>>> main #[contract] pub struct ScholarNFT; @@ -96,6 +139,21 @@ impl ScholarNFT { if env.storage().instance().has(&ADMIN_KEY) { panic_with_error!(&env, ScholarNFTError::AlreadyInitialized); } +<<<<<<< HEAD + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Counter, &0_u64); + + // Emit initialized event + env.events().publish( + (symbol_short!("init"),), + InitializedEventData { admin }, + ); + + Self::extend_instance(&env); + } + + /// Mint a new soulbound NFT. Only callable by admin. +======= admin.require_auth(); env.storage().instance().set(&ADMIN_KEY, &admin); upgrade::init(&env); @@ -109,6 +167,7 @@ impl ScholarNFT { Self::extend_instance(&env); } +>>>>>>> main pub fn mint(env: Env, to: Address, metadata_uri: String) -> u64 { let admin = Self::get_admin(&env); admin.require_auth(); @@ -119,6 +178,20 @@ impl ScholarNFT { panic_with_error!(&env, ScholarNFTError::TokenExists); } +<<<<<<< HEAD + env.storage().persistent().set(&key, &to); + env.storage().persistent().set(&DataKey::TokenUri(token_id), &uri); + + Self::extend_persistent(&env, &key); + Self::extend_persistent(&env, &DataKey::TokenUri(token_id)); + + // Emit minted event + env.events().publish( + (symbol_short!("minted"), token_id, to.clone()), + MintEventData { + owner: to, + metadata_uri: uri, +======= env.storage().persistent().set(&owner_key, &to); Self::extend_persistent(&env, &owner_key); @@ -142,12 +215,26 @@ impl ScholarNFT { MintEventData { token_id, owner: to, +>>>>>>> main }, ); token_id } +<<<<<<< HEAD + /// Revoke a credential. Only callable by admin. + pub fn revoke(env: Env, admin: Address, token_id: u64, reason: String) { + admin.require_auth(); + let stored_admin = Self::get_admin(&env); + if admin != stored_admin { + panic_with_error!(&env, Error::Unauthorized); + } + + let key = DataKey::Owner(token_id); + if !env.storage().persistent().has(&key) { + panic_with_error!(&env, Error::TokenNotFound); +======= pub fn revoke(env: Env, token_id: u64, reason: String) { let admin = Self::get_admin(&env); admin.require_auth(); @@ -155,16 +242,27 @@ impl ScholarNFT { let owner_key = DataKey::Owner(token_id); if !env.storage().persistent().has(&owner_key) { panic_with_error!(&env, ScholarNFTError::TokenNotFound); +>>>>>>> main } let revoked_key = DataKey::Revoked(token_id); if env.storage().persistent().has(&revoked_key) { +<<<<<<< HEAD + return; +======= panic_with_error!(&env, ScholarNFTError::AlreadyRevoked); +>>>>>>> main } env.storage().persistent().set(&revoked_key, &reason); Self::extend_persistent(&env, &revoked_key); +<<<<<<< HEAD + Self::extend_persistent(&env, &revoked_key); + + // Emit revoked event +======= +>>>>>>> main env.events().publish( (symbol_short!("revoked"), token_id), RevokedEventData { token_id, reason }, @@ -185,6 +283,12 @@ impl ScholarNFT { new_admin, }, ); +<<<<<<< HEAD + panic_with_error!(&env, Error::Soulbound) + } + + /// Returns the owner of the token. +======= Self::extend_instance(&env); } @@ -262,23 +366,45 @@ impl ScholarNFT { panic_with_error!(&env, ScholarNFTError::Soulbound); } +>>>>>>> main pub fn owner_of(env: Env, token_id: u64) -> Address { Self::extend_instance(&env); let revoked_key = DataKey::Revoked(token_id); if env.storage().persistent().has(&revoked_key) { Self::extend_persistent(&env, &revoked_key); +<<<<<<< HEAD + panic_with_error!(&env, Error::TokenRevoked); +======= panic_with_error!(&env, ScholarNFTError::TokenRevoked); +>>>>>>> main } - + let key = DataKey::Owner(token_id); if let Some(owner) = env.storage().persistent().get::<_, Address>(&key) { Self::extend_persistent(&env, &key); owner } else { panic_with_error!(&env, ScholarNFTError::TokenNotFound); +<<<<<<< HEAD } } + /// Returns the URI of the token. + pub fn token_uri(env: Env, token_id: u64) -> String { + let key = DataKey::TokenUri(token_id); + if let Some(uri) = env.storage().persistent().get::<_, String>(&key) { + uri + } else { + panic_with_error!(&env, Error::TokenNotFound); + } + } + + /// Returns true if the token is a valid credential (not revoked and exists). +======= + } + } + +>>>>>>> main pub fn has_credential(env: Env, token_id: u64) -> bool { Self::extend_instance(&env); let revoked_key = DataKey::Revoked(token_id); @@ -304,6 +430,11 @@ impl ScholarNFT { revoked } + /// Returns true if the token has been revoked. + pub fn is_revoked(env: Env, token_id: u64) -> bool { + env.storage().persistent().has(&DataKey::Revoked(token_id)) + } + pub fn get_revocation_reason(env: Env, token_id: u64) -> Option { Self::extend_instance(&env); let key = DataKey::Revoked(token_id); @@ -326,6 +457,17 @@ impl ScholarNFT { counter } + fn next_token_id(env: &Env) -> u64 { + let mut counter = env + .storage() + .instance() + .get(&TOKEN_COUNTER_KEY) + .unwrap_or(0_u64); + counter = counter.saturating_add(1); + env.storage().instance().set(&TOKEN_COUNTER_KEY, &counter); + counter + } + fn get_admin(env: &Env) -> Address { Self::extend_instance(env); env.storage() @@ -341,7 +483,13 @@ impl ScholarNFT { } fn extend_persistent(env: &Env, key: &DataKey) { +<<<<<<< HEAD + env.storage() + .persistent() + .extend_ttl(key, PERSISTENT_BUMP_THRESHOLD, PERSISTENT_EXTEND_TO); +======= env.storage().persistent().extend_ttl(key, TTL_MIN, TTL_MAX); +>>>>>>> main } } diff --git a/contracts/scholar_nft/src/test.rs b/contracts/scholar_nft/src/test.rs index d5900959..92f92adb 100644 --- a/contracts/scholar_nft/src/test.rs +++ b/contracts/scholar_nft/src/test.rs @@ -1,12 +1,20 @@ #![cfg(test)] use crate::{ +<<<<<<< HEAD + InitializedEventData, MintEventData, ScholarNFT, ScholarNFTClient, +}; +use soroban_sdk::{ + testutils::{Address as _, Events as _, MockAuth, MockAuthInvoke}, + Address, Env, IntoVal, String, symbol_short, +======= AdminChangedEventData, DataKey, InitializedEventData, MintEventData, ScholarMetadata, ScholarNFT, ScholarNFTClient, ScholarNFTError, }; use soroban_sdk::{ Address, BytesN, Env, IntoVal, String, symbol_short, testutils::{Address as _, Events as _, MockAuth, MockAuthInvoke, storage::Persistent}, +>>>>>>> main }; fn setup(env: &Env) -> (Address, Address, ScholarNFTClient) { @@ -204,6 +212,30 @@ fn token_uri_returns_metadata_uri() { } #[test] +<<<<<<< HEAD +#[should_panic(expected = "Error(Auth, InvalidAction)")] +fn non_admin_mint_panics() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let hacker = Address::generate(&env); + let scholar = Address::generate(&env); + + // hacker tries to mint - this will fail admin.require_auth() + env.mock_auths(&[MockAuth { + address: &hacker, + invoke: &MockAuthInvoke { + contract: &client.address, + fn_name: "mint", + args: (&scholar, cid(&env, "ipfs://hax")).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.mint(&scholar, &cid(&env, "ipfs://hax")); +} + +#[test] +======= fn get_metadata_uri_round_trip() { let env = Env::default(); let (_, _admin, client) = setup(&env); @@ -241,18 +273,26 @@ fn non_admin_mint_panics() { } #[test] +>>>>>>> main #[should_panic(expected = "Error(Contract, #1)")] fn test_double_initialize_reverts() { let env = Env::default(); let (_, admin, client) = setup(&env); +<<<<<<< HEAD +======= env.mock_all_auths(); +>>>>>>> main client.initialize(&admin); } #[test] fn test_revoke_flow() { let env = Env::default(); +<<<<<<< HEAD + let (_, admin, client) = setup(&env); +======= let (_, _admin, client) = setup(&env); +>>>>>>> main let recipient = Address::generate(&env); let reason = String::from_str(&env, "Cheater"); @@ -260,7 +300,11 @@ fn test_revoke_flow() { let token_id = client.mint(&recipient, &cid(&env, "ipfs://test")); assert!(client.has_credential(&token_id)); +<<<<<<< HEAD + client.revoke(&admin, &token_id, &reason); +======= client.revoke(&token_id, &reason); +>>>>>>> main assert!(!client.has_credential(&token_id)); assert!(client.is_revoked(&token_id)); @@ -271,13 +315,21 @@ fn test_revoke_flow() { #[should_panic(expected = "Error(Contract, #5)")] fn test_owner_of_revoked_fails() { let env = Env::default(); +<<<<<<< HEAD + let (_, admin, client) = setup(&env); +======= let (_, _admin, client) = setup(&env); +>>>>>>> main let recipient = Address::generate(&env); let reason = String::from_str(&env, "Plagiarism"); env.mock_all_auths(); let token_id = client.mint(&recipient, &cid(&env, "ipfs://test")); +<<<<<<< HEAD + client.revoke(&admin, &token_id, &reason); +======= client.revoke(&token_id, &reason); +>>>>>>> main client.owner_of(&token_id); } @@ -286,13 +338,33 @@ fn test_owner_of_revoked_fails() { #[should_panic(expected = "Error(Auth, InvalidAction)")] fn test_unauthorized_revoke_fails() { let env = Env::default(); +<<<<<<< HEAD + let (_, _admin, client) = setup(&env); +======= let (contract_id, _admin, client) = setup(&env); +>>>>>>> main let scholar = Address::generate(&env); let hacker = Address::generate(&env); let reason = String::from_str(&env, "Hax"); env.mock_all_auths(); let token_id = client.mint(&scholar, &cid(&env, "ipfs://test")); +<<<<<<< HEAD + + // hacker tries to revoke - mock_auths to mimic hacker's call + env.mock_auths(&[MockAuth { + address: &hacker, + invoke: &MockAuthInvoke { + contract: &client.address, + fn_name: "revoke", + args: (&hacker, token_id, reason.clone()).into_val(&env), + sub_invokes: &[], + }, + }]); + client.revoke(&hacker, &token_id, &reason); +} + +======= env.mock_auths(&[MockAuth { address: &hacker, @@ -306,30 +378,48 @@ fn test_unauthorized_revoke_fails() { client.revoke(&token_id, &reason); } +>>>>>>> main #[test] #[should_panic(expected = "Error(Contract, #4)")] fn test_revoke_non_existent_token_panics() { let env = Env::default(); +<<<<<<< HEAD + let (_, admin, client) = setup(&env); +======= let (_, _admin, client) = setup(&env); +>>>>>>> main let token_id = 999u64; let reason = String::from_str(&env, "Testing"); env.mock_all_auths(); +<<<<<<< HEAD + client.revoke(&admin, &token_id, &reason); +======= client.revoke(&token_id, &reason); +>>>>>>> main } #[test] #[should_panic(expected = "Error(Contract, #8)")] fn test_revoke_already_revoked_panics() { let env = Env::default(); +<<<<<<< HEAD + let (_, admin, client) = setup(&env); +======= let (_, _admin, client) = setup(&env); +>>>>>>> main let scholar = Address::generate(&env); let reason = String::from_str(&env, "Reason"); env.mock_all_auths(); let token_id = client.mint(&scholar, &cid(&env, "ipfs://test")); +<<<<<<< HEAD + client.revoke(&admin, &token_id, &reason); + client.revoke(&admin, &token_id, &reason); +======= client.revoke(&token_id, &reason); client.revoke(&token_id, &reason); +>>>>>>> main } #[test] @@ -344,12 +434,20 @@ fn initialize_emits_event() { let events = env.events().all(); let found = events.iter().any(|(_, topics, data)| { +<<<<<<< HEAD + topics.contains(&symbol_short!("init").into_val(&env)) + && { + let d: InitializedEventData = data.clone().into_val(&env); + d == InitializedEventData { admin: admin.clone() } + } +======= topics.contains(&symbol_short!("init").into_val(&env)) && { let d: InitializedEventData = data.clone().into_val(&env); d == InitializedEventData { admin: admin.clone(), } } +>>>>>>> main }); assert!(found, "initialized event not found"); } @@ -359,7 +457,7 @@ fn mint_emits_event() { let env = Env::default(); let (_, _admin, client) = setup(&env); let scholar = Address::generate(&env); - let uri = cid(&env, "ipfs://mint-event-test"); + let token_id = 1u64; env.mock_all_auths(); let token_id = client.mint(&scholar, &uri); @@ -370,16 +468,34 @@ fn mint_emits_event() { && topics.contains(&token_id.into_val(&env)) && { let d: MintEventData = data.clone().into_val(&env); +<<<<<<< HEAD + d == MintEventData { owner: scholar.clone(), metadata_uri: uri.clone() } +======= d == MintEventData { token_id, owner: scholar.clone(), } +>>>>>>> main } }); assert!(found, "mint event not found"); } #[test] +<<<<<<< HEAD +#[should_panic(expected = "Error(Contract, #7)")] +fn transfer_attempt_panics() { + let env = Env::default(); + let (_, _admin, client) = setup(&env); + let from = Address::generate(&env); + let to = Address::generate(&env); + let uri = cid(&env, "ipfs://test"); + + env.mock_all_auths(); + let token_id = client.mint(&from, &uri); + + client.transfer(&from, &to, &token_id); +======= fn transfer_admin_emits_event() { let env = Env::default(); let (_, old_admin, client) = setup(&env); @@ -417,6 +533,7 @@ fn transfer_panics_with_soulbound_error() { ScholarNFTError::Soulbound as u32 ))) ); +>>>>>>> main } #[test] @@ -425,6 +542,19 @@ fn transfer_attempt_reverts_soulbound() { let (_, _admin, client) = setup(&env); let from = Address::generate(&env); let to = Address::generate(&env); +<<<<<<< HEAD + let uri = cid(&env, "ipfs://test"); + + env.mock_all_auths(); + let token_id = client.mint(&from, &uri); + + // Use try_transfer to verify the specific Soulbound error (#7) + let res = client.try_transfer(&from, &to, &token_id); + assert!(res.is_err()); + + // Note: event emission on panic cannot be verified via env.events().all() + // as Soroban rolls back events on contract failure. +======= env.mock_all_auths(); let token_id = client.mint(&from, &cid(&env, "ipfs://test")); @@ -550,4 +680,5 @@ fn benchmark_costs() { std::println!("initialize: instr={}, mem={}", init_instr, init_mem); std::println!("mint: instr={}, mem={}", mint_instr, mint_mem); std::println!("get_all_scholars: instr={}, mem={}", get_instr, get_mem); +>>>>>>> main } diff --git a/contracts/scholarship_treasury/src/lib.rs b/contracts/scholarship_treasury/src/lib.rs index def48aaf..24e8512e 100644 --- a/contracts/scholarship_treasury/src/lib.rs +++ b/contracts/scholarship_treasury/src/lib.rs @@ -6,10 +6,13 @@ use soroban_sdk::{ contractimpl, contracttype, panic_with_error, symbol_short, }; +<<<<<<< HEAD +======= use learnvault_shared::upgrade; pub use upgrade::ContractUpgraded; +>>>>>>> main // --------------------------------------------------------------------------- // Storage Constants (assuming ~6s ledger time) // --------------------------------------------------------------------------- @@ -45,6 +48,8 @@ pub enum DataKey { Scholar(Address), VoteCast(u32, Address), // (proposal_id, voter) -> bool FinalizedProposal(u32), // proposal_id -> ProposalStatus (set by finalize_proposal) +<<<<<<< HEAD +======= } #[contractevent(topics = ["proposal_executed"])] @@ -62,6 +67,7 @@ pub struct ProposalCancelled { #[topic] pub proposal_id: u32, pub cancelled_by: Address, +>>>>>>> main } #[derive(Clone)] @@ -199,6 +205,10 @@ impl ScholarshipTreasury { env.storage().instance().set(&SCHOLARS_KEY, &0_u32); env.storage().instance().set(&DONORS_KEY, &0_u32); env.storage().instance().set(&PAUSED_KEY, &false); +<<<<<<< HEAD + + Self::extend_instance(&env); +======= env.storage() .instance() .set(&MIN_LRN_TO_PROPOSE_KEY, &0_i128); @@ -243,6 +253,7 @@ impl ScholarshipTreasury { panic_with_error!(&env, Error::InvalidAmount); } env.storage().instance().set(&APPROVAL_BPS_KEY, &new_bps); +>>>>>>> main } pub fn pause(env: Env) { @@ -319,6 +330,8 @@ impl ScholarshipTreasury { env.storage() .persistent() .set(&donor_key, &(current + amount)); + + Self::extend_persistent(&env, &donor_key); Self::extend_persistent(&env, &donor_key); @@ -581,6 +594,8 @@ impl ScholarshipTreasury { env.storage() .persistent() .set(&DataKey::Proposal(proposal_id), &proposal); + + Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); Self::extend_persistent(&env, &DataKey::Proposal(proposal_id)); @@ -594,7 +609,11 @@ impl ScholarshipTreasury { env.storage() .persistent() .set(&applicant_key, &proposal_ids); +<<<<<<< HEAD + +======= +>>>>>>> main Self::extend_persistent(&env, &applicant_key); env.storage() .instance() @@ -757,6 +776,12 @@ impl ScholarshipTreasury { } let total_votes = proposal.yes_votes + proposal.no_votes; +<<<<<<< HEAD + let quorum_met = total_gov > 0 + && total_votes + .checked_mul(10_000) + .map(|tv| tv / total_gov >= MIN_QUORUM_BPS) +======= let quorum_threshold = Self::get_quorum(env.clone()); let approval_bps = Self::get_approval_bps(env.clone()); @@ -766,6 +791,7 @@ impl ScholarshipTreasury { .yes_votes .checked_mul(10_000) .map(|v| (v / total_votes) as u32 > approval_bps) +>>>>>>> main .unwrap_or(false); let status = if passed { @@ -777,6 +803,8 @@ impl ScholarshipTreasury { env.storage() .persistent() .set(&DataKey::FinalizedProposal(proposal_id), &status.clone()); + + Self::extend_persistent(&env, &DataKey::FinalizedProposal(proposal_id)); Self::extend_persistent(&env, &DataKey::FinalizedProposal(proposal_id)); diff --git a/contracts/upgrade_timelock_vault/src/lib.rs b/contracts/upgrade_timelock_vault/src/lib.rs index c3391ae4..ab6203ad 100644 --- a/contracts/upgrade_timelock_vault/src/lib.rs +++ b/contracts/upgrade_timelock_vault/src/lib.rs @@ -264,7 +264,11 @@ impl UpgradeTimelockVault { /// Returns true if the timelock has expired for the given contract. pub fn is_upgrade_ready(env: Env, contract_address: Address) -> bool { if let Some(proposal) = Self::get_upgrade_proposal(env.clone(), contract_address) { +<<<<<<< HEAD + let timelock_duration = Self::get_timelock_duration(env.clone()); +======= let config = Self::get_config(&env); +>>>>>>> main let current_time = env.ledger().timestamp(); current_time >= proposal.queued_at + config.timelock_duration } else { @@ -288,7 +292,11 @@ impl UpgradeTimelockVault { #[cfg(test)] mod test { use super::*; +<<<<<<< HEAD + use soroban_sdk::testutils::{Address as _, BytesN as _, Ledger}; +======= use soroban_sdk::testutils::{Address as _, Ledger}; +>>>>>>> main use soroban_sdk::{Address, BytesN, Env, IntoVal, contractclient}; #[contractclient(name = "UpgradeTimelockVaultClient")] @@ -324,10 +332,15 @@ mod test { fn test_initialize() { let env = create_env(); let admin = create_admin(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -336,6 +349,14 @@ mod test { } #[test] +<<<<<<< HEAD + #[should_panic(expected = "Error(Contract, #1)")] + fn test_initialize_twice_fails() { + let env = create_env(); + let admin = create_admin(&env); + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= #[should_panic(expected = "Error(Contract, #6)")] fn test_initialize_twice_fails() { let env = create_env(); @@ -344,6 +365,7 @@ mod test { &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); contract.initialize(&admin); @@ -353,10 +375,15 @@ mod test { fn test_set_timelock_duration() { let env = create_env(); let admin = create_admin(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -381,10 +408,15 @@ mod test { let env = create_env(); let admin = create_admin(&env); let unauthorized = create_admin(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -406,11 +438,17 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main + env.ledger().set_timestamp(1000); contract.initialize(&admin); env.ledger().set_timestamp(1); @@ -439,10 +477,15 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -475,10 +518,15 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -513,10 +561,15 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -542,10 +595,15 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -583,10 +641,15 @@ mod test { let admin = create_admin(&env); let contract_addr = create_contract(&env); let wasm_hash = create_wasm_hash(&env); +<<<<<<< HEAD + let contract = + UpgradeTimelockVaultClient::new(&env, &env.register(UpgradeTimelockVault {}, ())); +======= let contract = UpgradeTimelockVaultClient::new( &env, &env.register_contract(None, UpgradeTimelockVault {}), ); +>>>>>>> main contract.initialize(&admin); @@ -615,6 +678,8 @@ mod test { // Now ready assert!(contract.is_upgrade_ready(&contract_addr)); } +<<<<<<< HEAD +======= #[test] fn benchmark_costs() { @@ -666,4 +731,5 @@ mod test { std::println!("queue_upgrade: instr={}, mem={}", queue_instr, queue_mem); std::println!("execute_upgrade: instr={}, mem={}", exec_instr, exec_mem); } +>>>>>>> main } diff --git a/docs/contracts.md b/docs/contracts.md index 6a3d8873..e682dc4d 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -89,9 +89,12 @@ Contracts must be deployed in this order due to cross-contract dependencies: The `UpgradeTimelockVault` implements a dedicated vault pattern for secure contract upgrades with timelock enforcement. +<<<<<<< HEAD +======= For the current V1 in-place upgrade procedure used by the six core contracts, see [contract-upgrades.md](./contract-upgrades.md). +>>>>>>> main ### Security Model diff --git a/docs/token-economics.md b/docs/token-economics.md index 9d45c89f..72dc7984 100644 --- a/docs/token-economics.md +++ b/docs/token-economics.md @@ -1,13 +1,33 @@ # Token Economics +<<<<<<< HEAD +LearnVault uses two tokens because it has two distinct problems to solve: measuring learning (reputation) and governing scholarship disbursement (voting power). Conflating them into one token would break both functions. +======= LearnVault uses two tokens because it has two distinct problems to solve: measuring learning (reputation) and governing scholarship disbursement (voting power). Conflating them into one token would break both functions. +>>>>>>> main --- ## LRN (LearnToken) +<<<<<<< HEAD +LRN is not a financial asset. It is an on-chain reputation score — a number that says how much verified learning a wallet has completed inside the LearnVault system. It cannot be sent, sold, or delegated. + +### How it's earned + +LRN is minted by the `CourseMilestone` contract when a validator approves a milestone submission. The amount minted per milestone is set per track by the admin committee in V1 — there is no global fixed rate. A learner completing a beginner track will earn less LRN than one completing an advanced engineering track. Exact amounts per track are configured at course creation time via `add_course`. + +### What it unlocks + +| Threshold | What it enables | +|---|---| +| Configurable per track | Scholarship eligibility — wallet can be nominated | +| Governance threshold | Eligibility to participate in DAO votes on proposals | + +Reaching these thresholds does not automatically grant anything — it makes the wallet *eligible*. Scholarship disbursement still requires a passing governance vote (see GOV below). +======= LRN is not a financial asset. It is an on-chain reputation score — a number that says how much verified learning a wallet has completed inside the LearnVault system. It cannot be sent, sold, or delegated. @@ -31,11 +51,19 @@ track. Exact amounts per track are configured at course creation time via Reaching these thresholds does not automatically grant anything — it makes the wallet _eligible_. Scholarship disbursement still requires a passing governance vote (see GOV below). +>>>>>>> main ### Why it's non-transferable If LRN could be transferred, the following would happen immediately: +<<<<<<< HEAD +- Wallets with capital but no learning would buy reputation and access scholarships meant for real learners +- A secondary market would form around eligibility thresholds, pricing out genuine participants +- Sybil attackers could launder reputation across fresh wallets to reset eligibility windows + +Soulbound design is not ideological — it is the only mechanism that makes the eligibility threshold meaningful. A score you cannot buy is the only score worth having. +======= - Wallets with capital but no learning would buy reputation and access scholarships meant for real learners - A secondary market would form around eligibility thresholds, pricing out @@ -46,26 +74,48 @@ If LRN could be transferred, the following would happen immediately: Soulbound design is not ideological — it is the only mechanism that makes the eligibility threshold meaningful. A score you cannot buy is the only score worth having. +>>>>>>> main ### Supply model - **Cap:** None. LRN is uncapped. +<<<<<<< HEAD +- **Minting:** Exclusively by `contracts/course_milestone/` — no other contract or admin can mint LRN directly. +- **Burning:** No burn mechanic. LRN balances are permanent records of completed work. +======= - **Minting:** Exclusively by `contracts/course_milestone/` — no other contract or admin can mint LRN directly. - **Burning:** No burn mechanic. LRN balances are permanent records of completed work. +>>>>>>> main --- ## GOV (GovernanceToken) +<<<<<<< HEAD +GOV is voting weight in the scholarship DAO. Unlike LRN, it is a transferable token — deliberately so. +======= GOV is voting weight in the scholarship DAO. Unlike LRN, it is a transferable token — deliberately so. +>>>>>>> main ### How it's earned GOV is minted through two paths: +<<<<<<< HEAD +1. **Donation:** 1 USDC deposited to the treasury mints 1 GOV. Donors get governance rights proportional to their contribution. +2. **Learner rewards:** Wallets that cross the top-learner LRN threshold receive a GOV distribution as a reward. This gives high-performing learners a voice in how scholarship funds are allocated. + +### What it does + +GOV holders vote on scholarship disbursement proposals. Votes are weighted by GOV balance. A proposal must reach a quorum and a majority to pass. In V1, proposal creation is permissioned — only wallets above the LRN governance threshold or holding minimum GOV can submit proposals. + +### Why it IS transferable + +Donors need an exit. Locking capital permanently into a governance token with no liquidity would deter serious donors from participating. Transferability also creates secondary market price discovery — if GOV trades at a premium, it is a signal that the community values governance rights, which attracts more donors. If it trades at a discount, that is honest feedback about protocol health. +======= 1. **Donation:** 1 USDC deposited to the treasury mints 1 GOV. Donors get governance rights proportional to their contribution. 2. **Learner rewards:** Wallets that cross the top-learner LRN threshold receive @@ -86,6 +136,7 @@ liquidity would deter serious donors from participating. Transferability also creates secondary market price discovery — if GOV trades at a premium, it is a signal that the community values governance rights, which attracts more donors. If it trades at a discount, that is honest feedback about protocol health. +>>>>>>> main Transferability is a feature, not a compromise. @@ -96,11 +147,15 @@ Transferability is a feature, not a compromise. > ⚠️ **Open design question** > +<<<<<<< HEAD +> The GOV burn mechanic has not been finalized. Options under consideration include burning GOV when a scholarship is disbursed (aligning token supply with treasury outflows), burning on governance participation as a spam deterrent, or no burn at all. This will be resolved before mainnet. Track the discussion in [#139](https://github.com/bakeronchain/learnvault/issues/139). +======= > The GOV burn mechanic has not been finalized. Options under consideration > include burning GOV when a scholarship is disbursed (aligning token supply > with treasury outflows), burning on governance participation as a spam > deterrent, or no burn at all. This will be resolved before mainnet. Track the > discussion in [#139](https://github.com/bakeronchain/learnvault/issues/139). +>>>>>>> main --- @@ -140,8 +195,12 @@ The two tokens are designed to reinforce each other through a feedback loop: [More GOV minted → governance decentralizes] ────┘ ``` +<<<<<<< HEAD +The loop only holds if LRN remains non-transferable. The moment reputation can be bought, step one becomes pay-to-win and the rest of the flywheel breaks. +======= The loop only holds if LRN remains non-transferable. The moment reputation can be bought, step one becomes pay-to-win and the rest of the flywheel breaks. +>>>>>>> main --- @@ -149,6 +208,14 @@ be bought, step one becomes pay-to-win and the rest of the flywheel breaks. V1 ships with the following centralized components. None of this is hidden: +<<<<<<< HEAD +- **Milestone approval** is controlled by a validator committee. There is no on-chain dispute resolution. A validator can reject a valid submission and there is currently no appeal mechanism. +- **Minting permissions** on `contracts/course_milestone/` are set by an admin key. The admin can add courses, set milestone counts, and configure LRN amounts per track. +- **Scholarship disbursement** requires a multisig in V1. Even if a proposal passes governance, the actual USDC transfer goes through a multisig held by the core team. +- **Contract upgrades** are not yet governed on-chain. The team can upgrade contracts unilaterally. + +This is the honest state of V1. It ships this way because the alternative — launching with incomplete decentralization infrastructure and calling it trustless — is worse. +======= - **Milestone approval** is controlled by a validator committee. There is no on-chain dispute resolution. A validator can reject a valid submission and there is currently no appeal mechanism. @@ -164,6 +231,7 @@ V1 ships with the following centralized components. None of this is hidden: This is the honest state of V1. It ships this way because the alternative — launching with incomplete decentralization infrastructure and calling it trustless — is worse. +>>>>>>> main ### V2 Roadmap @@ -174,23 +242,39 @@ Before admin keys are removed, the following needs to exist: 3. A validator election mechanism governed by GOV holders 4. Time-locked upgrade governance so contract changes require a passing vote +<<<<<<< HEAD +V2 decentralization is not a vague future commitment — it is a prerequisite for removing the admin keys. Until those components exist, the keys stay and this document says so plainly. +======= V2 decentralization is not a vague future commitment — it is a prerequisite for removing the admin keys. Until those components exist, the keys stay and this document says so plainly. +>>>>>>> main --- ## Contract References +<<<<<<< HEAD +| Contract | Path | Role | +|---|---|---| +| `CourseMilestone` | `contracts/course_milestone/` | Milestone approval, LRN minting | +| `ScholarNFT` | `contracts/scholar_nft/` | Soulbound credential on completion | +| Governance (planned) | `contracts/governance/` | GOV voting, proposal execution | +======= | Contract | Path | Role | | -------------------- | ----------------------------- | ---------------------------------- | | `CourseMilestone` | `contracts/course_milestone/` | Milestone approval, LRN minting | | `ScholarNFT` | `contracts/scholar_nft/` | Soulbound credential on completion | | Governance (planned) | `contracts/governance/` | GOV voting, proposal execution | +>>>>>>> main --- ## Further Reading - [README](../README.md) +<<<<<<< HEAD +- [Issue #139 — Token economics explainer](https://github.com/bakeronchain/learnvault/issues/139) +======= - [Issue #139 — Token economics explainer](https://github.com/bakeronchain/learnvault/issues/139) +>>>>>>> main diff --git a/i18next-scanner.config.js b/i18next-scanner.config.js new file mode 100644 index 00000000..702efaa4 --- /dev/null +++ b/i18next-scanner.config.js @@ -0,0 +1,23 @@ +module.exports = { + input: ["src/**/*.{ts,tsx}"], + output: "./src/locales/$LOCALE.json", + options: { + debug: false, + func: { + list: ["t", "i18n.t"], + extensions: [".ts", ".tsx"], + }, + lngs: ["en", "fr", "sw", "ps"], + ns: ["translation"], + defaultLng: "en", + defaultNs: "translation", + resource: { + loadPath: "src/locales/{{lng}}.json", + savePath: "src/locales/{{lng}}.json", + }, + keySeparator: false, + namespaceSeparator: false, + pluralSeparator: "", + contextSeparator: "", + }, +} diff --git a/package-lock.json b/package-lock.json index 8c738330..1b3597cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1349,6 +1349,73 @@ "node": ">=20.0.0" } }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@stellar/stellar-sdk": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", + "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@stellar/stellar-base": "^13.1.0", + "axios": "^1.8.4", + "bignumber.js": "^9.3.0", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@stellar/stellar-sdk/node_modules/@stellar/stellar-base": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", + "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "sodium-native": "^4.3.3" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@trezor/connect-plugin-stellar": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-plugin-stellar/-/connect-plugin-stellar-9.2.1.tgz", + "integrity": "sha512-Orz5gFZzYFZs1+cTsgg8fz/VWFjhl7pqMCqD5DVNZpXW+wrjwBaRbcGJZ+ibkPKU3AlM7Uv3SVD/pjaQmAkZ2Q==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/utils": "9.4.1" + }, + "peerDependencies": { + "@stellar/stellar-sdk": "^13.3.0", + "@trezor/connect": "9.x.x", + "tslib": "^2.6.2" + } + }, + "node_modules/@creit.tech/stellar-wallets-kit/node_modules/@trezor/utils": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.1.tgz", + "integrity": "sha512-9MYNa99tzXiTBnKadABoY2D80YL9Mh3ntM5wziwVhjZ4HyhqFH6BsCxwFpWYLUIKBctD55QEdE4bASoqp7Ad1A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, "node_modules/@creit.tech/xbull-wallet-connect": { "version": "0.4.0", "dependencies": { @@ -1440,6 +1507,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1484,6 +1552,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1778,25 +1847,1328 @@ "version": "2.0.1", "license": "MIT", "dependencies": { - "@noble/hashes": "2.0.1" + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, +<<<<<<< HEAD + "node_modules/@expo/cli": { + "version": "55.0.18", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.18.tgz", + "integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.1.1", + "@expo/image-utils": "^0.8.12", + "@expo/json-file": "^10.0.12", + "@expo/log-box": "55.0.7", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~55.0.11", + "@expo/osascript": "^2.4.2", + "@expo/package-manager": "^1.10.3", + "@expo/plist": "^0.5.2", + "@expo/prebuild-config": "^55.0.10", + "@expo/require-utils": "^55.0.3", + "@expo/router-server": "^55.0.11", + "@expo/schema-utils": "^55.0.2", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.4.0", + "@react-native/dev-middleware": "0.83.2", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "dnssd-advertise": "^1.1.3", + "expo-server": "^55.0.6", + "fetch-nodeshim": "^0.4.6", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.2.0", + "multitars": "^0.2.3", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^4.0.3", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "terminal-link": "^2.1.1", + "toqr": "^0.1.1", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1", + "zod": "^3.25.76" + }, + "bin": { + "expo-internal": "build/bin/cli" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@expo/cli/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/cli/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/cli/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@expo/cli/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", + "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-forge": "^1.3.3" + } + }, + "node_modules/@expo/config": { + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.10.tgz", + "integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config-plugins": "~55.0.7", + "@expo/config-types": "^55.0.5", + "@expo/json-file": "^10.0.12", + "@expo/require-utils": "^55.0.3", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-from": "^5.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4" + } + }, + "node_modules/@expo/config-plugins": { + "version": "55.0.7", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.7.tgz", + "integrity": "sha512-XZUoDWrsHEkH3yasnDSJABM/UxP5a1ixzRwU/M+BToyn/f0nTrSJJe/Ay/FpxkI4JSNz2n0e06I23b2bleXKVA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config-types": "^55.0.5", + "@expo/json-file": "~10.0.12", + "@expo/plist": "^0.5.2", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/config-plugins/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/config-plugins/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/config-plugins/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/config-types": { + "version": "55.0.5", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.5.tgz", + "integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/devcert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", + "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/devtools": { + "version": "55.0.2", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-55.0.2.tgz", + "integrity": "sha512-4VsFn9MUriocyuhyA+ycJP3TJhUsOFHDc270l9h3LhNpXMf6wvIdGcA0QzXkZtORXmlDybWXRP2KT1k36HcQkA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^4.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/devtools/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/devtools/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/devtools/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/dom-webview": { + "version": "55.0.3", + "resolved": "https://registry.npmjs.org/@expo/dom-webview/-/dom-webview-55.0.3.tgz", + "integrity": "sha512-bY4/rfcZ0f43DvOtMn8/kmPlmo01tex5hRoc5hKbwBwQjqWQuQt0ACwu7akR9IHI4j0WNG48eL6cZB6dZUFrzg==", + "license": "MIT", + "optional": true, + "peer": true, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/env": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.1.tgz", + "integrity": "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "getenv": "^2.0.0" + }, + "engines": { + "node": ">=20.12.0" + } + }, + "node_modules/@expo/env/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/env/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/env/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/fingerprint": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.16.6.tgz", + "integrity": "sha512-nRITNbnu3RKSHPvKVehrSU4KG2VY9V8nvULOHBw98ukHCAU4bGrU5APvcblOkX3JAap+xEHsg/mZvqlvkLInmQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/env": "^2.0.11", + "@expo/spawn-async": "^1.7.2", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "ignore": "^5.3.1", + "minimatch": "^10.2.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/fingerprint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/fingerprint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/fingerprint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/fingerprint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", + "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + } + }, + "node_modules/@expo/image-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/image-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/image-utils/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/image-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/json-file": { + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", + "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/local-build-cache-provider": { + "version": "55.0.7", + "resolved": "https://registry.npmjs.org/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.7.tgz", + "integrity": "sha512-Qg9uNZn1buv4zJUA4ZQaz+ZnKDCipRgjoEg2Gcp8Qfy+2Gq5yZKX4YN1TThCJ01LJk/pvJsCRxXlXZSwdZppgg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config": "~55.0.10", + "chalk": "^4.1.2" + } + }, + "node_modules/@expo/local-build-cache-provider/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/local-build-cache-provider/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/local-build-cache-provider/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/log-box": { + "version": "55.0.7", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.7.tgz", + "integrity": "sha512-m7V1k2vlMp4NOj3fopjOg4zl/ANXyTRF3HMTMep2GZAKsPiDzgOQ41nm8CaU50/HlDIGXlCObss07gOn20UpHQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/dom-webview": "^55.0.3", + "anser": "^1.4.9", + "stacktrace-parser": "^0.1.10" + }, + "peerDependencies": { + "@expo/dom-webview": "^55.0.3", + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/metro": { + "version": "54.2.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", + "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3" + } + }, + "node_modules/@expo/metro-config": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-55.0.11.tgz", + "integrity": "sha512-qGxq7RwWpj0zNvZO/e5aizKrOKYYBrVPShSbxPOVB1EXcexxTPTxnOe4pYFg/gKkLIJe0t3jSSF8IDWlGdaaOg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~55.0.10", + "@expo/env": "~2.1.1", + "@expo/json-file": "~10.0.12", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.32.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/metro-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/metro-config/node_modules/hermes-estree": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.1.tgz", + "integrity": "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/metro-config/node_modules/hermes-parser": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.1.tgz", + "integrity": "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.32.1" + } + }, + "node_modules/@expo/metro-config/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@expo/metro-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/metro-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/osascript": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", + "integrity": "sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/spawn-async": "^1.7.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.10.3.tgz", + "integrity": "sha512-ZuXiK/9fCrIuLjPSe1VYmfp0Sa85kCMwd8QQpgyi5ufppYKRtLBg14QOgUqj8ZMbJTxE0xqzd0XR7kOs3vAK9A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/json-file": "^10.0.12", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/package-manager/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/package-manager/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/package-manager/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/plist": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.2.tgz", + "integrity": "sha512-o4xdVdBpe4aTl3sPMZ2u3fJH4iG1I768EIRk1xRZP+GaFI93MaR3JvoFibYqxeTmLQ1p1kNEVqylfUjezxx45g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@expo/prebuild-config": { + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz", + "integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", + "@expo/config-types": "^55.0.5", + "@expo/image-utils": "^0.8.12", + "@expo/json-file": "^10.0.12", + "@react-native/normalize-colors": "0.83.2", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/@expo/prebuild-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/require-utils": { + "version": "55.0.3", + "resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.3.tgz", + "integrity": "sha512-TS1m5tW45q4zoaTlt6DwmdYHxvFTIxoLrTHKOFrIirHIqIXnHCzpceg8wumiBi+ZXSaGY2gobTbfv+WVhJY6Fw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@expo/router-server": { + "version": "55.0.11", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.11.tgz", + "integrity": "sha512-Kd8J1OOlFR00DZxn+1KfiQiXZtRut6cj8+ynqHJa7dtt/lTL4tGkYistqmVhpKJ6w886eRY5WivKy7o0ZBFkJA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@expo/metro-runtime": "^55.0.6", + "expo": "*", + "expo-constants": "^55.0.9", + "expo-font": "^55.0.4", + "expo-router": "*", + "expo-server": "^55.0.6", + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + }, + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/@expo/schema-utils": { + "version": "55.0.2", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-55.0.2.tgz", + "integrity": "sha512-QZ5WKbJOWkCrMq0/kfhV9ry8te/OaS34YgLVpG8u9y2gix96TlpRTbxM/YATjNcUR2s4fiQmPCOxkGtog4i37g==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/spawn-async": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", + "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/vector-icons": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", + "license": "MIT", + "optional": true, + "peer": true, + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@expo/xcpretty": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.1.tgz", + "integrity": "sha512-KZNxZvnGCtiM2aYYZ6Wz0Ix5r47dAvpNLApFtZWnSoERzAdOMzVBOPysBoM0JlF6FKWZ8GPqgn6qt3dV/8Zlpg==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.20.0", + "chalk": "^4.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@expo/xcpretty/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/xcpretty/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 20.19.0" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { - "version": "2.0.1", + "node_modules/@expo/xcpretty/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=8" } }, +======= +>>>>>>> main "node_modules/@fivebinaries/coin-selection": { "version": "3.0.0", "license": "Apache-2.0", @@ -1914,6 +3286,26 @@ } }, "node_modules/@ledgerhq/devices": { +<<<<<<< HEAD + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.13.0.tgz", + "integrity": "sha512-hgGn1kpe/rT0EJ0Qs7rG+1TXA4g6HN2t3dB4DndRTqVqC9aSSbME+ajA0QWLZisxOD3zkwvO4Q0mJ2zARAKyag==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/errors": "^6.32.0", + "@ledgerhq/logs": "^6.16.0", + "rxjs": "7.8.2", + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/devices/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" +======= "version": "8.6.1", "license": "Apache-2.0", "dependencies": { @@ -1921,10 +3313,16 @@ "@ledgerhq/logs": "^6.13.0", "rxjs": "^7.8.1", "semver": "^7.3.5" +>>>>>>> main } }, "node_modules/@ledgerhq/errors": { "version": "6.32.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.32.0.tgz", + "integrity": "sha512-BjjvhLM6UXYUbhllqAduo9PSneLt9FXZ3TBEUFQ3MMSZOCHt0gAgDySLwul99R8fdYWkXBza4DYQjUNckpN2lg==", +======= +>>>>>>> main "license": "Apache-2.0" }, "node_modules/@ledgerhq/hw-app-str": { @@ -2020,7 +3418,6 @@ "resolved": "https://registry.npmjs.org/@near-js/accounts/-/accounts-1.4.1.tgz", "integrity": "sha512-ni3QT9H3NdrbVVKyx56yvz93r89Dvpc/vgVtiIK2OdXjkK6jcj+UKMDRQ6F7rd9qJOInLkHZbVBtcR6j1CXLjw==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/providers": "1.0.3", @@ -2040,8 +3437,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@near-js/crypto": { "version": "1.4.2", @@ -2064,7 +3460,6 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores/-/keystores-0.2.2.tgz", "integrity": "sha512-DLhi/3a4qJUY+wgphw2Jl4S+L0AKsUYm1mtU0WxKYV5OBwjOXvbGrXNfdkheYkfh3nHwrQgtjvtszX6LrRXLLw==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/types": "0.3.1" @@ -2075,7 +3470,6 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores-browser/-/keystores-browser-0.2.2.tgz", "integrity": "sha512-Pxqm7WGtUu6zj32vGCy9JcEDpZDSB5CCaLQDTQdF3GQyL0flyRv2I/guLAgU5FLoYxU7dJAX9mslJhPW7P2Bfw==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2" @@ -2086,7 +3480,6 @@ "resolved": "https://registry.npmjs.org/@near-js/keystores-node/-/keystores-node-0.1.2.tgz", "integrity": "sha512-MWLvTszZOVziiasqIT/LYNhUyWqOJjDGlsthOsY6dTL4ZcXjjmhmzrbFydIIeQr+CcEl5wukTo68ORI9JrHl6g==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2" @@ -2097,7 +3490,6 @@ "resolved": "https://registry.npmjs.org/@near-js/providers/-/providers-1.0.3.tgz", "integrity": "sha512-VJMboL14R/+MGKnlhhE3UPXCGYvMd1PpvF9OqZ9yBbulV7QVSIdTMfY4U1NnDfmUC2S3/rhAEr+3rMrIcNS7Fg==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/transactions": "1.3.3", "@near-js/types": "0.3.1", @@ -2113,8 +3505,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@near-js/providers/node_modules/node-fetch": { "version": "2.6.7", @@ -2122,7 +3513,6 @@ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2138,40 +3528,11 @@ } } }, - "node_modules/@near-js/providers/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@near-js/providers/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true, - "peer": true - }, - "node_modules/@near-js/providers/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/@near-js/signers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@near-js/signers/-/signers-0.2.2.tgz", "integrity": "sha512-M6ib+af9zXAPRCjH2RyIS0+RhCmd9gxzCeIkQ+I2A3zjgGiEDkBZbYso9aKj8Zh2lPKKSH7h+u8JGymMOSwgyw==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/keystores": "0.2.2", @@ -2183,7 +3544,6 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 16" }, @@ -2196,7 +3556,6 @@ "resolved": "https://registry.npmjs.org/@near-js/transactions/-/transactions-1.3.3.tgz", "integrity": "sha512-1AXD+HuxlxYQmRTLQlkVmH+RAmV3HwkAT8dyZDu+I2fK/Ec9BQHXakOJUnOBws3ihF+akQhamIBS5T0EXX/Ylw==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/crypto": "1.4.2", "@near-js/signers": "0.2.2", @@ -2210,8 +3569,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@near-js/types": { "version": "0.3.1", @@ -2232,7 +3590,6 @@ "resolved": "https://registry.npmjs.org/@near-js/wallet-account/-/wallet-account-1.3.3.tgz", "integrity": "sha512-GDzg/Kz0GBYF7tQfyQQQZ3vviwV8yD+8F2lYDzsWJiqIln7R1ov0zaXN4Tii86TeS21KPn2hHAsVu3Y4txa8OQ==", "license": "ISC", - "peer": true, "dependencies": { "@near-js/accounts": "1.4.1", "@near-js/crypto": "1.4.2", @@ -2249,8 +3606,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-1.0.0.tgz", "integrity": "sha512-fSVWzzemnyfF89EPwlUNsrS5swF5CrtiN4e+h0/lLf4dz2he4L3ndM20PS9wj7ICSkXJe/TQUHdaPTq15b1mNQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@near-wallet-selector/core": { "version": "8.10.2", @@ -2272,7 +3628,21 @@ "version": "0.9.0", "license": "MIT" }, +<<<<<<< HEAD + "node_modules/@ngneat/elf": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@ngneat/elf/-/elf-2.5.1.tgz", + "integrity": "sha512-13BItNZFgHglTiXuP9XhisNczwQ5QSzH+imAv9nAPsdbCq/3ortqkIYRnlxB8DGPVcuIjLujQ4OcZa+9QWgZtw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "rxjs": ">=7.0.0" + } + }, + "node_modules/@ngneat/elf-devtools": { +======= "node_modules/@noble/ciphers": { +>>>>>>> main "version": "1.3.0", "license": "MIT", "engines": { @@ -3558,6 +4928,7 @@ "node_modules/@solana/kit": { "version": "2.3.0", "license": "MIT", + "peer": true, "dependencies": { "@solana/accounts": "2.3.0", "@solana/addresses": "2.3.0", @@ -4115,6 +5486,7 @@ "node_modules/@solana/sysvars": { "version": "2.3.0", "license": "MIT", + "peer": true, "dependencies": { "@solana/accounts": "2.3.0", "@solana/codecs": "2.3.0", @@ -4225,6 +5597,7 @@ "node_modules/@solana/web3.js": { "version": "1.98.4", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -4309,6 +5682,7 @@ "node_modules/@stellar/stellar-base": { "version": "14.1.0", "license": "Apache-2.0", + "peer": true, "dependencies": { "@noble/curves": "^1.9.6", "@stellar/js-xdr": "^3.1.2", @@ -4763,6 +6137,8 @@ "version": "0.6.1", "license": "MIT" }, +<<<<<<< HEAD +======= "node_modules/@trezor/analytics": { "version": "1.5.0", "license": "See LICENSE.md in repo root", @@ -4901,6 +6277,7 @@ "tslib": "^2.6.2" } }, +>>>>>>> main "node_modules/@trezor/connect": { "version": "9.7.2", "license": "SEE LICENSE IN LICENSE.md", @@ -4943,40 +6320,384 @@ "tslib": "^2.6.2" } }, - "node_modules/@trezor/connect-analytics": { - "version": "1.4.0", + "node_modules/@trezor/connect-analytics": { + "version": "1.4.0", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/analytics": "1.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-analytics/node_modules/@trezor/analytics": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/analytics/-/analytics-1.4.2.tgz", + "integrity": "sha512-FgjJekuDvx1TjiDemvpnPiRck7Kp/v1ZeppsBYpQR3yGKyKzbG1pVpcl0RyI2237raXxbORaz7XV8tcyjq4BXg==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@mobily/ts-belt": "^3.13.1", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/env-utils": "1.5.0", + "@trezor/protobuf": "1.5.2", + "@trezor/utils": "9.5.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-analytics/node_modules/@trezor/env-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.4.2.tgz", + "integrity": "sha512-lQvrqcNK5I4dy2MuiLyMuEm0KzY59RIu2GLtc9GsvqyxSPZkADqVzGeLJjXj/vI2ajL8leSpMvmN4zPw3EK8AA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/utils": "9.5.0", + "@trezor/utxo-lib": "2.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link/node_modules/@trezor/blockchain-link-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.5.1.tgz", + "integrity": "sha512-2tDGLEj5jzydjsJQONGTWVmCDDy6FTZ4ytr1/2gE6anyYEJU8MbaR+liTt3UvcP5jwZTNutwYLvZixRfrb8JpA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@mobily/ts-belt": "^3.13.1", + "@stellar/stellar-sdk": "14.2.0", + "@trezor/env-utils": "1.5.0", + "@trezor/protobuf": "1.5.1", + "@trezor/utils": "9.5.0", + "xrpl": "4.4.3" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link/node_modules/@trezor/protobuf": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.1.tgz", + "integrity": "sha512-nAkaCCAqLpErBd+IuKeG5MpbyLR/2RMgCw18TWc80m1Ws/XgQirhHY9Jbk6gLImTXb9GTrxP0+MDSahzd94rSA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/schema-utils": "1.4.0", + "long": "5.2.5", + "protobufjs": "7.4.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-common": { + "version": "0.5.1", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { +<<<<<<< HEAD + "@trezor/env-utils": "1.4.2", + "@trezor/utils": "9.4.2" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-common/node_modules/@trezor/env-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.4.2.tgz", + "integrity": "sha512-lQvrqcNK5I4dy2MuiLyMuEm0KzY59RIu2GLtc9GsvqyxSPZkADqVzGeLJjXj/vI2ajL8leSpMvmN4zPw3EK8AA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/analytics": "1.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-common": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@trezor/connect-common/-/connect-common-0.5.1.tgz", + "integrity": "sha512-wdpVCwdylBh4SBO5Ys40tB/d59UlfjmxgBHDkkLgaR+JcqkthCfiw5VlUrV9wu65lquejAZhA5KQL4mUUUhCow==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/env-utils": "1.5.0", + "@trezor/type-utils": "1.2.0", + "@trezor/utils": "9.5.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/blockchain-link-utils/node_modules/@trezor/utils": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.2.tgz", + "integrity": "sha512-Fm3m2gmfXsgv4chqn5HX8e8dElEr2ibBJSJ7HE3bsHh/1OSQcDdzsSioAK04Fo9ws/v7n6lt+QBZ6fGmwyIkZQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/@trezor/websocket-client": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-1.2.2.tgz", + "integrity": "sha512-vu9L1V/5yh8LHQCmsGC9scCnihELsVuR5Tri1IvW3CdgTUFFcfjsEgXsFqFME3HlxuUmx6qokw0Gx/o0/hzaSQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/utils": "9.4.2", + "ws": "^8.18.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@stellar/stellar-base": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.1.0.tgz", + "integrity": "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.1.2", + "buffer": "^6.0.3", + "sha.js": "^2.3.6", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "sodium-native": "^4.3.3" + } + }, + "node_modules/@trezor/connect/node_modules/@stellar/stellar-sdk": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.3.0.tgz", + "integrity": "sha512-8+GHcZLp+mdin8gSjcgfb/Lb6sSMYRX6Nf/0LcSJxvjLQR0XHpjGzOiRbYb2jSXo51EnA6kAV5j+4Pzh5OUKUg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^13.1.0", + "axios": "^1.8.4", + "bignumber.js": "^9.3.0", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/blockchain-link": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link/-/blockchain-link-2.5.2.tgz", + "integrity": "sha512-/egUnIt/fR57QY33ejnkPMhZwRvVRS/pUCoqdVIGitN1Q7QZsdopoR4hw37hdK/Ux/q1ZLH6LZz7U2UFahjppw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/@trezor/connect/-/connect-9.6.2.tgz", + "integrity": "sha512-XsSERBK+KnF6FPsATuhB9AEM0frekVLwAwFo35MRV9I4P+mdv6tnUiZUq8O8aoPbfJwDjtNJSYv+PMsKuRH6rg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@ethereumjs/common": "^10.0.0", + "@ethereumjs/tx": "^10.0.0", + "@fivebinaries/coin-selection": "3.0.0", + "@mobily/ts-belt": "^3.13.1", + "@noble/hashes": "^1.6.1", + "@scure/bip39": "^1.5.1", + "@solana-program/compute-budget": "^0.8.0", + "@solana-program/system": "^0.7.0", + "@solana-program/token": "^0.5.1", + "@solana-program/token-2022": "^0.4.2", + "@solana/kit": "^2.1.1", + "@trezor/blockchain-link": "2.5.2", + "@trezor/blockchain-link-types": "1.4.2", + "@trezor/blockchain-link-utils": "1.4.2", + "@trezor/connect-analytics": "1.3.5", + "@trezor/connect-common": "0.4.2", + "@trezor/crypto-utils": "1.1.4", + "@trezor/device-utils": "1.1.2", + "@trezor/env-utils": "^1.4.2", + "@trezor/protobuf": "1.4.2", + "@trezor/protocol": "1.2.8", + "@trezor/schema-utils": "1.3.4", + "@trezor/transport": "1.5.2", + "@trezor/type-utils": "1.1.8", + "@trezor/utils": "9.4.2", + "@trezor/utxo-lib": "2.4.2", + "blakejs": "^1.2.1", + "bs58": "^6.0.0", + "bs58check": "^4.0.0", + "cross-fetch": "^4.0.0", + "jws": "^4.0.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/blockchain-link-types": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-types/-/blockchain-link-types-1.4.2.tgz", + "integrity": "sha512-KThBmGOFLJAFnmou9ThQhnjEVxfYPfEwMOaVTVNgJ+NAkt5rEMx0SKBBelCGZ63XtOLWdVPglFo83wtm+I9Vpg==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "@trezor/analytics": "1.4.2" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/blockchain-link-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/blockchain-link-utils/-/blockchain-link-utils-1.4.2.tgz", + "integrity": "sha512-PBEBrdtHn0dn/c9roW6vjdHI/CucMywJm5gthETZAZmzBOtg6ZDpLTn+qL8+jZGIbwcAkItrQ3iHrHhR6xTP5g==", "license": "See LICENSE.md in repo root", "dependencies": { - "@trezor/analytics": "1.5.0" + "@trezor/env-utils": "1.4.2", + "@trezor/utils": "9.4.2" }, "peerDependencies": { "tslib": "^2.6.2" } }, - "node_modules/@trezor/connect-common": { - "version": "0.5.1", + "node_modules/@trezor/connect/node_modules/@trezor/crypto-utils": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@trezor/crypto-utils/-/crypto-utils-1.1.4.tgz", + "integrity": "sha512-Y6VziniqMPoMi70IyowEuXKqRvBYQzgPAekJaUZTHhR+grtYNRKRH2HJCvuZ8MGmSKUFSYfa7y8AvwALA8mQmA==", "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/device-utils": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.1.2.tgz", + "integrity": "sha512-R3AJvAo+a3wYVmcGZO2VNl9PZOmDEzCZIlmCJn0BlSRWWd8G9u1qyo/fL9zOwij/YhCaJyokmSHmIEmbY9qpgw==", + "license": "See LICENSE.md in repo root" + }, + "node_modules/@trezor/connect/node_modules/@trezor/env-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/env-utils/-/env-utils-1.4.2.tgz", + "integrity": "sha512-lQvrqcNK5I4dy2MuiLyMuEm0KzY59RIu2GLtc9GsvqyxSPZkADqVzGeLJjXj/vI2ajL8leSpMvmN4zPw3EK8AA==", + "license": "See LICENSE.md in repo root", + "dependencies": { + "ua-parser-js": "^2.0.4" + }, + "peerDependencies": { + "expo-constants": "*", + "expo-localization": "*", + "react-native": "*", + "tslib": "^2.6.2" + }, + "peerDependenciesMeta": { + "expo-constants": { + "optional": true + }, + "expo-localization": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/protobuf": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.4.2.tgz", + "integrity": "sha512-AeIYKCgKcE9cWflggGL8T9gD+IZLSGrwkzqCk3wpIiODd5dUCgEgA4OPBufR6OMu3RWu/Tgu2xviHunijG3LXQ==", + "license": "See LICENSE.md in repo root", "dependencies": { + "bignumber.js": "^9.3.0" +======= "@trezor/env-utils": "1.5.0", "@trezor/type-utils": "1.2.0", "@trezor/utils": "9.5.0" +>>>>>>> main }, "peerDependencies": { "tslib": "^2.6.2" } }, +<<<<<<< HEAD + "node_modules/@trezor/connect/node_modules/@trezor/protocol": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-1.2.8.tgz", + "integrity": "sha512-8EH+EU4Z1j9X4Ljczjbl9G7vVgcUz41qXcdE+6FOG3BFvMDK4KUVvaOtWqD+1dFpeo5yvWSTEKdhgXMPFprWYQ==", + "license": "See LICENSE.md in repo root", +======= "node_modules/@trezor/connect-plugin-stellar": { "version": "9.2.6", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@trezor/utils": "9.5.0" }, +>>>>>>> main "peerDependencies": { "@stellar/stellar-sdk": "^13.3.0", "@trezor/connect": "9.x.x", "tslib": "^2.6.2" } }, +<<<<<<< HEAD + "node_modules/@trezor/connect/node_modules/@trezor/transport": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@trezor/transport/-/transport-1.5.2.tgz", + "integrity": "sha512-rYP87zdVll2bNBtsD3VxJq0yjaNvIClcgszZjQwVTQxpKGFPkx8bLSpAGI05R9qfxusZJCfYarjX3qki9nHYPw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@trezor/connect": "9.6.2", + "@trezor/connect-common": "0.4.2", + "@trezor/utils": "9.4.2", + "@trezor/websocket-client": "1.2.2" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect/node_modules/@trezor/type-utils": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@trezor/type-utils/-/type-utils-1.1.8.tgz", + "integrity": "sha512-VtvkPXpwtMtTX9caZWYlMMTmhjUeDq4/1LGn0pSdjd4OuL/vQyuPWXCT/0RtlnRraW6R2dZF7rX2UON2kQIMTQ==", + "license": "See LICENSE.md in repo root" + }, + "node_modules/@trezor/connect/node_modules/@trezor/websocket-client": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@trezor/websocket-client/-/websocket-client-1.2.2.tgz", + "integrity": "sha512-vu9L1V/5yh8LHQCmsGC9scCnihELsVuR5Tri1IvW3CdgTUFFcfjsEgXsFqFME3HlxuUmx6qokw0Gx/o0/hzaSQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.0" + }, + "peerDependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@trezor/connect-web/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/@trezor/connect-web/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" +======= "node_modules/@trezor/connect-web": { "version": "9.7.2", "license": "SEE LICENSE IN LICENSE.md", @@ -4988,6 +6709,7 @@ }, "peerDependencies": { "tslib": "^2.6.2" +>>>>>>> main } }, "node_modules/@trezor/connect/node_modules/base-x": { @@ -5001,13 +6723,58 @@ "base-x": "^5.0.0" } }, +<<<<<<< HEAD + "node_modules/@trezor/device-authenticity/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "ua-parser-js": "^2.0.4" + }, +======= "node_modules/@trezor/crypto-utils": { "version": "1.2.0", "license": "SEE LICENSE IN LICENSE.md", +>>>>>>> main "peerDependencies": { + "expo-constants": "*", + "expo-localization": "*", + "react-native": "*", "tslib": "^2.6.2" + }, + "peerDependenciesMeta": { + "expo-constants": { + "optional": true + }, + "expo-localization": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@trezor/device-authenticity/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "extraneous": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, +<<<<<<< HEAD + "node_modules/@trezor/device-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@trezor/device-utils/-/device-utils-1.2.0.tgz", + "integrity": "sha512-Aqp7pIooFTx21zRUtTI6i1AS4d9Lrx7cclvksh2nJQF9WJvbzuCXshEGkLoOsHwhQrCl3IXfbGuMdA12yDenPA==", +======= "node_modules/@trezor/device-authenticity": { "version": "1.1.2", "license": "See LICENSE.md in repo root", @@ -5044,6 +6811,7 @@ }, "node_modules/@trezor/device-utils": { "version": "1.2.0", +>>>>>>> main "license": "See LICENSE.md in repo root" }, "node_modules/@trezor/env-utils": { @@ -5072,6 +6840,11 @@ }, "node_modules/@trezor/protobuf": { "version": "1.5.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@trezor/protobuf/-/protobuf-1.5.2.tgz", + "integrity": "sha512-zViaL1jKue8DUTVEDg0C/lMipqNMd/Z3kr29/+MeZOoupjaXIQ2Lqp3WAMe8hvNTKKX8aNQH9JrbapJ6w9FMXw==", +======= +>>>>>>> main "license": "See LICENSE.md in repo root", "dependencies": { "@trezor/schema-utils": "1.4.0", @@ -5084,6 +6857,11 @@ }, "node_modules/@trezor/protocol": { "version": "1.3.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@trezor/protocol/-/protocol-1.3.0.tgz", + "integrity": "sha512-rmrxbDrdgxTouBPbZcSeqU7ba/e5WVT1dxvxxEntHqRdTiDl7d3VK+BErCrlyol8EH5YCqEF3/rXt0crSOfoFw==", +======= +>>>>>>> main "license": "See LICENSE.md in repo root", "peerDependencies": { "tslib": "^2.6.2" @@ -5091,6 +6869,11 @@ }, "node_modules/@trezor/schema-utils": { "version": "1.4.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@trezor/schema-utils/-/schema-utils-1.4.0.tgz", + "integrity": "sha512-K7upSeh7VDrORaIC4KAxYVW93XNlohmUnH5if/5GKYmTdQSRp1nBkO6Jm+Z4hzIthdnz/1aLgnbeN3bDxWLRxA==", +======= +>>>>>>> main "license": "See LICENSE.md in repo root", "dependencies": { "@sinclair/typebox": "^0.33.7", @@ -5100,6 +6883,15 @@ "tslib": "^2.6.2" } }, +<<<<<<< HEAD + "node_modules/@trezor/utils": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@trezor/utils/-/utils-9.4.2.tgz", + "integrity": "sha512-Fm3m2gmfXsgv4chqn5HX8e8dElEr2ibBJSJ7HE3bsHh/1OSQcDdzsSioAK04Fo9ws/v7n6lt+QBZ6fGmwyIkZQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "bignumber.js": "^9.3.1" +======= "node_modules/@trezor/transport": { "version": "1.6.2", "license": "SEE LICENSE IN LICENSE.md", @@ -5110,6 +6902,7 @@ "@trezor/utils": "9.5.0", "cross-fetch": "^4.0.0", "usb": "^2.15.0" +>>>>>>> main }, "peerDependencies": { "tslib": "^2.6.2" @@ -5131,6 +6924,11 @@ }, "node_modules/@trezor/utxo-lib": { "version": "2.5.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@trezor/utxo-lib/-/utxo-lib-2.5.0.tgz", + "integrity": "sha512-Fa2cZh0037oX6AHNLfpFIj65UR/OoX0ZJTocFuQASe77/1PjZHysf6BvvGfmzuFToKfrAQ+DM/1Sx+P/vnyNmA==", +======= +>>>>>>> main "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@trezor/utils": "9.5.0", @@ -5166,6 +6964,8 @@ "base-x": "^5.0.0" } }, +<<<<<<< HEAD +======= "node_modules/@trezor/websocket-client": { "version": "1.3.0", "license": "SEE LICENSE IN LICENSE.md", @@ -5272,6 +7072,7 @@ } } }, +>>>>>>> main "node_modules/@tybys/wasm-util": { "version": "0.10.1", "license": "MIT", @@ -5284,6 +7085,54 @@ "version": "5.0.4", "devOptional": true, "license": "MIT" +<<<<<<< HEAD + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } +======= +>>>>>>> main }, "node_modules/@types/canvas-confetti": { "version": "1.9.0", @@ -5295,8 +7144,7 @@ "devOptional": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "@types/d3-color": "*" } }, "node_modules/@types/connect": { @@ -5313,7 +7161,13 @@ }, "node_modules/@types/d3-color": { "version": "3.1.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "devOptional": true, +======= "dev": true, +>>>>>>> main "license": "MIT" }, "node_modules/@types/d3-ease": { @@ -5324,10 +7178,14 @@ "node_modules/@types/d3-interpolate": { "version": "3.0.4", "dev": true, +<<<<<<< HEAD + "license": "MIT" +======= "license": "MIT", "dependencies": { "@types/d3-color": "*" } +>>>>>>> main }, "node_modules/@types/d3-path": { "version": "1.0.11", @@ -5367,11 +7225,14 @@ "@types/ms": "*" } }, +<<<<<<< HEAD +======= "node_modules/@types/deep-eql": { "version": "4.0.2", "devOptional": true, "license": "MIT" }, +>>>>>>> main "node_modules/@types/estree": { "version": "1.0.8", "license": "MIT" @@ -5453,6 +7314,7 @@ "node_modules/@types/react": { "version": "19.2.14", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5461,6 +7323,7 @@ "version": "19.2.3", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5536,6 +7399,7 @@ "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.2", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", @@ -5569,6 +7433,7 @@ "node_modules/@typescript-eslint/parser": { "version": "8.57.2", "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -6030,6 +7895,11 @@ }, "node_modules/@vitest/coverage-v8": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", +======= +>>>>>>> main "dev": true, "license": "MIT", "dependencies": { @@ -6087,6 +7957,11 @@ }, "node_modules/@vitest/expect": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6103,6 +7978,11 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6128,6 +8008,11 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6139,6 +8024,11 @@ }, "node_modules/@vitest/runner": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6151,6 +8041,11 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6165,6 +8060,11 @@ }, "node_modules/@vitest/spy": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "funding": { @@ -6173,6 +8073,11 @@ }, "node_modules/@vitest/utils": { "version": "4.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "dependencies": { @@ -6922,6 +8827,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7000,6 +8906,11 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", +======= +>>>>>>> main "license": "MIT", "engines": { "node": ">=8" @@ -7007,6 +8918,11 @@ }, "node_modules/ansi-styles": { "version": "5.2.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", +======= +>>>>>>> main "devOptional": true, "license": "MIT", "engines": { @@ -7179,6 +9095,11 @@ }, "node_modules/asn1.js": { "version": "4.10.1", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "bn.js": "^4.0.0", @@ -7188,6 +9109,11 @@ }, "node_modules/asn1.js/node_modules/bn.js": { "version": "4.12.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/assert": { @@ -7202,6 +9128,8 @@ "util": "^0.12.5" } }, +<<<<<<< HEAD +======= "node_modules/assertion-error": { "version": "2.0.1", "devOptional": true, @@ -7210,6 +9138,7 @@ "node": ">=12" } }, +>>>>>>> main "node_modules/ast-v8-to-istanbul": { "version": "1.0.0", "dev": true, @@ -7345,6 +9274,11 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.10.11", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", +======= +>>>>>>> main "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -7489,6 +9423,11 @@ }, "node_modules/browserify-aes": { "version": "1.2.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "buffer-xor": "^1.0.3", @@ -7501,6 +9440,11 @@ }, "node_modules/browserify-cipher": { "version": "1.0.1", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "browserify-aes": "^1.0.4", @@ -7510,6 +9454,11 @@ }, "node_modules/browserify-des": { "version": "1.0.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "cipher-base": "^1.0.1", @@ -7520,6 +9469,11 @@ }, "node_modules/browserify-rsa": { "version": "4.1.1", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "bn.js": "^5.2.1", @@ -7532,6 +9486,11 @@ }, "node_modules/browserify-sign": { "version": "4.2.5", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", +======= +>>>>>>> main "license": "ISC", "dependencies": { "bn.js": "^5.2.2", @@ -7550,10 +9509,20 @@ }, "node_modules/browserify-sign/node_modules/isarray": { "version": "1.0.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/browserify-sign/node_modules/readable-stream": { "version": "2.3.8", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -7567,10 +9536,20 @@ }, "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/browserify-sign/node_modules/string_decoder": { "version": "1.1.1", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -7578,6 +9557,11 @@ }, "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/browserify-zlib": { @@ -7605,6 +9589,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7673,6 +9658,11 @@ }, "node_modules/buffer-xor": { "version": "1.0.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/bufferutil": { @@ -7680,6 +9670,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -7784,6 +9775,11 @@ }, "node_modules/cbor": { "version": "10.0.12", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", + "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "nofilter": "^3.0.2" @@ -8052,6 +10048,19 @@ "node": ">=12" } }, +<<<<<<< HEAD + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, +======= +>>>>>>> main "node_modules/cliui/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -8289,6 +10298,11 @@ }, "node_modules/create-ecdh": { "version": "4.0.4", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -8297,6 +10311,11 @@ }, "node_modules/create-ecdh/node_modules/bn.js": { "version": "4.12.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/create-hash": { @@ -8360,6 +10379,8 @@ "node": "*" } }, +<<<<<<< HEAD +======= "node_modules/crypto-browserify": { "version": "3.12.0", "license": "MIT", @@ -8389,6 +10410,7 @@ "node": ">=8" } }, +>>>>>>> main "node_modules/css-tree": { "version": "3.2.1", "dev": true, @@ -8786,6 +10808,11 @@ }, "node_modules/des.js": { "version": "1.1.0", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -8838,6 +10865,11 @@ }, "node_modules/diffie-hellman": { "version": "5.0.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "bn.js": "^4.1.0", @@ -8847,6 +10879,11 @@ }, "node_modules/diffie-hellman/node_modules/bn.js": { "version": "4.12.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", +======= +>>>>>>> main "license": "MIT" }, "node_modules/dijkstrajs": { @@ -8877,8 +10914,16 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "devOptional": true, + "license": "MIT", + "peer": true +======= "devOptional": true, "license": "MIT" +>>>>>>> main }, "node_modules/domain-browser": { "version": "4.22.0", @@ -9654,6 +11699,11 @@ }, "node_modules/evp_bytestokey": { "version": "1.0.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", +======= +>>>>>>> main "license": "MIT", "dependencies": { "md5.js": "^1.3.4", @@ -9716,8 +11766,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/extend": { "version": "3.0.2", @@ -10010,7 +12059,6 @@ "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", "license": "MIT", - "peer": true, "dependencies": { "is-property": "^1.0.0" } @@ -10243,9 +12291,12 @@ "license": "MIT", "engines": { "node": ">= 0.4" +<<<<<<< HEAD +======= }, "funding": { "url": "https://github.com/sponsors/ljharb" +>>>>>>> main } }, "node_modules/graceful-fs": { @@ -10526,7 +12577,6 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "license": "MIT", - "peer": true, "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -10543,7 +12593,6 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -10552,8 +12601,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/https-browserify": { "version": "1.0.0", @@ -10607,6 +12655,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.29.2" }, @@ -10627,8 +12676,16 @@ } }, "node_modules/idb-keyval": { +<<<<<<< HEAD + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0", + "peer": true +======= "version": "6.2.1", "license": "Apache-2.0" +>>>>>>> main }, "node_modules/ieee754": { "version": "1.2.1", @@ -11001,15 +13058,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-my-json-valid": { "version": "2.20.6", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", "license": "MIT", - "peer": true, "dependencies": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", @@ -11103,8 +13158,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -11576,7 +13630,6 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11990,6 +14043,11 @@ }, "node_modules/log-update/node_modules/ansi-styles": { "version": "6.2.3", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", +======= +>>>>>>> main "dev": true, "license": "MIT", "engines": { @@ -12044,8 +14102,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -12936,29 +14993,15 @@ } } }, - "node_modules/near-api-js/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "peer": true - }, - "node_modules/near-api-js/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/near-api-js/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", + "optional": true, "peer": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "engines": { + "node": ">= 0.6" } }, "node_modules/node-addon-api": { @@ -13816,6 +15859,17 @@ "devOptional": true, "license": "MIT" }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "dev": true, @@ -15152,6 +17206,19 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, +<<<<<<< HEAD + "node_modules/slugify": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.8.tgz", + "integrity": "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, +======= "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "6.2.3", "dev": true, @@ -15167,6 +17234,7 @@ "version": "0.3.2", "license": "MIT" }, +>>>>>>> main "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", @@ -15509,6 +17577,11 @@ }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "6.2.2", +<<<<<<< HEAD + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", +======= +>>>>>>> main "dev": true, "license": "MIT", "engines": { @@ -15518,6 +17591,8 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, +<<<<<<< HEAD +======= "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15527,6 +17602,7 @@ "node": ">=6" } }, +>>>>>>> main "node_modules/strip-indent": { "version": "3.0.0", "dev": true, @@ -15550,6 +17626,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/style-to-js": { "version": "1.1.21", "license": "MIT", @@ -15827,6 +17911,12 @@ } }, "node_modules/tr46": { +<<<<<<< HEAD + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" +======= "version": "6.0.0", "dev": true, "license": "MIT", @@ -15836,6 +17926,7 @@ "engines": { "node": ">=20" } +>>>>>>> main }, "node_modules/tree-kill": { "version": "1.2.2", @@ -16099,7 +18190,13 @@ "license": "MIT" }, "node_modules/undici": { +<<<<<<< HEAD + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", +======= "version": "7.24.6", +>>>>>>> main "dev": true, "license": "MIT", "engines": { @@ -16883,14 +18980,51 @@ "node": ">=18" } }, +<<<<<<< HEAD + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" +======= "node_modules/webidl-conversions": { "version": "8.0.1", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" +>>>>>>> main + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "dev": true, @@ -16900,18 +19034,29 @@ } }, "node_modules/whatwg-url": { +<<<<<<< HEAD + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", +======= "version": "16.0.1", "dev": true, +>>>>>>> main "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, +<<<<<<< HEAD + "node_modules/whatwg-url-minimum": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url-minimum/-/whatwg-url-minimum-0.1.1.tgz", + "integrity": "sha512-u2FNVjFVFZhdjb502KzXy1gKn1mEisQRJssmSJT8CPhZdZa0AP6VCbWlXERKyGu0l09t0k50FiDiralpGhBxgA==", + "license": "MIT", + "optional": true, + "peer": true +======= "node_modules/whatwg-url/node_modules/@exodus/bytes": { "version": "1.15.0", "dev": true, @@ -16942,6 +19087,7 @@ "funding": { "url": "https://paulmillr.com/funding/" } +>>>>>>> main }, "node_modules/which": { "version": "2.0.2", @@ -17229,6 +19375,19 @@ "node": ">=12" } }, +<<<<<<< HEAD + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, +======= +>>>>>>> main "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "dev": true, diff --git a/package.json b/package.json index 96941665..0c79d819 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "test:frontend": "vitest run", "test:contracts": "cargo test --workspace", "test:watch": "cargo watch -x 'test --workspace'", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "i18n:scan": "npx i18next-scanner --config ./i18next-scanner.config.js", + "i18n:pseudo": "node scripts/generate-pseudo-locale.mjs" }, "workspaces": [ "packages/*" @@ -92,7 +94,8 @@ "vite": "^8.0.3", "vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-wasm": "^3.5.0", - "vitest": "^4.1.1" + "vitest": "^4.1.1", + "i18next-scanner": "^4.3.1" }, "lint-staged": { "**/*": [ diff --git a/scripts/generate-pseudo-locale.mjs b/scripts/generate-pseudo-locale.mjs new file mode 100644 index 00000000..28471f51 --- /dev/null +++ b/scripts/generate-pseudo-locale.mjs @@ -0,0 +1,27 @@ +import fs from "fs" +import path from "path" + +const enPath = path.resolve("src/locales/en.json") +const psPath = path.resolve("src/locales/ps.json") +const enJson = JSON.parse(fs.readFileSync(enPath, "utf8")) + +const transformString = (value) => { + if (typeof value !== "string") return value + const preserved = value.replace(/{{\s*([^}]+)\s*}}/g, "{{$1}}") + return `[[${preserved}]]` +} + +const transformValue = (value) => { + if (typeof value === "string") return transformString(value) + if (Array.isArray(value)) return value.map(transformValue) + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, transformValue(item)]), + ) + } + return value +} + +const pseudo = transformValue(enJson) +fs.writeFileSync(psPath, JSON.stringify(pseudo, null, "\t"), "utf8") +console.log(`Pseudo-locale generated at ${psPath}`) diff --git a/server/package.json b/server/package.json index a02616c5..066279ee 100644 --- a/server/package.json +++ b/server/package.json @@ -10,10 +10,15 @@ "test": "jest --runInBand", "migrate": "ts-node scripts/migrate.ts up", "migrate:rollback": "ts-node scripts/migrate.ts down", +<<<<<<< HEAD + "db:migrate": "npm run migrate", + "db:seed": "ts-node scripts/seed.ts" +======= "migrate:verify": "ts-node scripts/verify-migrations.ts", "db:migrate": "npm run migrate", "db:seed": "ts-node scripts/seed.ts", "db:query:analyze": "ts-node scripts/query-analysis.ts" +>>>>>>> main }, "dependencies": { "@pinata/sdk": "^2.1.0", diff --git a/server/scripts/migrate.ts b/server/scripts/migrate.ts index 0bfb9f87..4c1c47b4 100644 --- a/server/scripts/migrate.ts +++ b/server/scripts/migrate.ts @@ -13,7 +13,11 @@ import fs from "node:fs" import path from "node:path" import dotenv from "dotenv" +<<<<<<< HEAD +import { Pool, PoolClient } from "pg" +======= import { Pool, type PoolClient } from "pg" +>>>>>>> main dotenv.config({ path: path.resolve(__dirname, "../.env") }) @@ -44,9 +48,13 @@ async function migrateUp(): Promise { const { rows: applied } = await client.query<{ filename: string }>( "SELECT filename FROM schema_migrations ORDER BY filename", ) +<<<<<<< HEAD + const appliedSet = new Set(applied.map((r: { filename: string }) => r.filename)) +======= const appliedSet = new Set( applied.map((r: { filename: string }) => r.filename), ) +>>>>>>> main const files = fs .readdirSync(MIGRATIONS_DIR) @@ -119,9 +127,16 @@ async function migrateDown(): Promise { await client.query("BEGIN") try { await client.query(sql) +<<<<<<< HEAD + await client.query( + "DELETE FROM schema_migrations WHERE filename = $1", + [last], + ) +======= await client.query("DELETE FROM schema_migrations WHERE filename = $1", [ last, ]) +>>>>>>> main await client.query("COMMIT") console.log(`\nRolled back: ${last}`) } catch (err) { diff --git a/server/src/controllers/admin-courses.controller.ts b/server/src/controllers/admin-courses.controller.ts new file mode 100644 index 00000000..cd5342ee --- /dev/null +++ b/server/src/controllers/admin-courses.controller.ts @@ -0,0 +1,302 @@ +import { type Request, type Response } from "express" +import { z } from "zod" +import { pool } from "../db/index" +import { AppError } from "../errors/app-error-handler" +import { + courseBulkImportBodySchema, + difficultyValues, +} from "../lib/zod-schemas" + +interface CourseImportRow { + title: string + slug: string + track: string + difficulty: string + description?: string + coverImage?: string | null + published?: boolean +} + +interface CourseImportResult { + row: number + slug: string + success: boolean + errors: string[] + course?: { + id: number + slug: string + title: string + description: string + coverImage: string | null + track: string + difficulty: string + published: boolean + createdAt: string + updatedAt: string + } +} + +const parseCsv = (csvText: string): Array> => { + const lines = csvText + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + if (lines.length === 0) { + return [] + } + + const headers = lines[0] + .split(",") + .map((header) => header.trim().replace(/\s+/g, "")) + + return lines.slice(1).map((line) => { + const columns = line.split(",").map((col) => col.trim()) + const row: Record = {} + headers.forEach((name, index) => { + row[name] = columns[index] ?? "" + }) + return row + }) +} + +const normalizeCsvRow = (row: Record) => ({ + title: row.title ?? row.Title ?? "", + slug: row.slug ?? row.Slug ?? "", + track: row.track ?? row.Track ?? "", + difficulty: row.difficulty ?? row.Difficulty ?? "", + description: row.description ?? row.Description ?? "", + coverImage: row.coverImage ?? row.CoverImage ?? null, + published: + row.published?.toLowerCase() === "true" || + row.published?.toLowerCase() === "yes" || + row.published?.toLowerCase() === "1" +}) + +const getClient = async () => { + if (typeof (pool as any).connect === "function") { + return await (pool as any).connect() + } + return pool as unknown as { query: typeof pool.query; release?: () => void } +} + +const buildResult = ( + rowIndex: number, + slug: string, + success: boolean, + errors: string[], + course?: CourseImportResult["course"], +): CourseImportResult => ({ + row: rowIndex + 1, + slug, + success, + errors, + course, +}) + +export const bulkImportCourses = async ( + req: Request, + res: Response, +): Promise => { + try { + const body = req.body as unknown + const parseResult = courseBulkImportBodySchema.safeParse(body) + if (!parseResult.success) { + throw new AppError( + "Validation failed", + 400, + parseResult.error.issues.map((issue) => ({ + field: issue.path.join(".") || "body", + message: issue.message, + })), + ) + } + + const requestData = parseResult.data + let courses: CourseImportRow[] = [] + if ("csv" in requestData) { + courses = parseCsv(requestData.csv).map(normalizeCsvRow) + } else { + courses = requestData.courses + } + + const rowErrors: CourseImportResult[] = [] + const normalizedRows: CourseImportRow[] = [] + + for (const [index, row] of courses.entries()) { + if (!row || typeof row !== "object") { + rowErrors.push( + buildResult(index, String(row?.slug ?? `row-${index + 1}`), false, ["Invalid row format"]), + ) + continue + } + + const validation = z + .object({ + title: z.string().trim().min(1, "title is required"), + slug: z + .string() + .trim() + .min(1, "slug is required") + .regex(/^[a-zA-Z0-9-_]+$/, "slug may contain only letters, numbers, hyphens, and underscores"), + track: z.string().trim().min(1, "track is required"), + difficulty: z + .string() + .trim() + .transform((value) => value.toLowerCase()), + description: z.string().optional(), + coverImage: z + .string() + .trim() + .min(1) + .optional() + .nullable(), + published: z.boolean().optional(), + }) + .strict() + .safeParse(row) + + const errors: string[] = [] + if (!validation.success) { + for (const issue of validation.error.issues) { + errors.push(issue.message) + } + } else { + if (!difficultyValues.has(validation.data.difficulty)) { + errors.push( + `difficulty must be one of: ${Array.from(difficultyValues).join(", ")}`, + ) + } + if (errors.length === 0) { + normalizedRows.push(validation.data) + } + } + + rowErrors.push( + buildResult( + index, + String(row.slug ?? `row-${index + 1}`), + errors.length === 0, + errors, + ), + ) + } + + const duplicateSlugMap = normalizedRows.reduce>( + (acc, row, index) => { + const slug = row.slug.toLowerCase() + acc[slug] = acc[slug] ?? [] + acc[slug].push(index) + return acc + }, + {}, + ) + + for (const [slug, indexes] of Object.entries(duplicateSlugMap)) { + if (indexes.length > 1) { + for (const index of indexes) { + rowErrors[index].success = false + rowErrors[index].errors.push("Duplicate slug found in upload payload") + } + } + } + + const rowsToInsert = rowErrors + .filter((row) => row.success) + .map((row) => row.row - 1) + + if (rowsToInsert.length > 0) { + const slugs = rowsToInsert.map((index) => normalizedRows[index].slug.toLowerCase()) + const existing = await pool.query( + `SELECT slug FROM courses WHERE LOWER(slug) = ANY($1::text[])`, + [slugs], + ) + for (const row of rowErrors) { + if (!row.success) continue + if ( + existing.rows.some( + (record: { slug: string }) => + record.slug.toLowerCase() === row.slug.toLowerCase(), + ) + ) { + row.success = false + row.errors.push("A course with this slug already exists") + } + } + } + + const needsInsert = rowErrors.some((row) => row.success) + const previewOnly = requestData.preview === true + + if (!previewOnly && needsInsert) { + const client = await getClient() + try { + await client.query("BEGIN") + const insertedCourses: CourseImportResult[] = [] + for (const rowIndex of rowErrors + .filter((row) => row.success) + .map((row) => row.row - 1)) { + const row = normalizedRows[rowIndex] + const result = await client.query( + `INSERT INTO courses (title, slug, description, cover_image_url, track, difficulty, published_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, slug, title, description, cover_image_url, track, difficulty, published_at, created_at, updated_at`, + [ + row.title, + row.slug, + row.description ?? "", + row.coverImage ?? null, + row.track, + row.difficulty, + row.published ? new Date().toISOString() : null, + ], + ) + const course = result.rows[0] + insertedCourses.push( + buildResult(rowIndex, row.slug, true, [], { + id: course.id, + slug: course.slug, + title: course.title, + description: course.description, + coverImage: course.cover_image_url, + track: course.track, + difficulty: course.difficulty, + published: Boolean(course.published_at), + createdAt: course.created_at, + updatedAt: course.updated_at, + }), + ) + } + await client.query("COMMIT") + for (const inserted of insertedCourses) { + const existingIndex = rowErrors.findIndex( + (row) => row.row === inserted.row && row.slug === inserted.slug, + ) + if (existingIndex !== -1) { + rowErrors[existingIndex] = inserted + } + } + } catch (err) { + await client.query("ROLLBACK") + throw err + } finally { + client.release?.() + } + } + + res.status(200).json({ + results: rowErrors, + total: rowErrors.length, + imported: rowErrors.filter((row) => row.success).length, + }) + } catch (error) { + if (error instanceof AppError) { + res.status(error.statusCode).json({ errors: error.details ?? [{ message: error.message }] }) + return + } + + console.error("[admin-courses] bulk import error", error) + res.status(500).json({ error: "Internal server error" }) + } +} diff --git a/server/src/controllers/governance.controller.ts b/server/src/controllers/governance.controller.ts index f9f01115..beb324cd 100644 --- a/server/src/controllers/governance.controller.ts +++ b/server/src/controllers/governance.controller.ts @@ -233,6 +233,17 @@ const castVoteSchema = z.object({ signature: z.string().optional(), }) +const castVoteSchema = z.object({ + proposal_id: z.number().int().positive("proposal_id must be a positive integer"), + voter_address: z + .string() + .min(56, "voter_address must be a valid Stellar address") + .max(56, "voter_address must be a valid Stellar address") + .startsWith("G", "voter_address must be a valid Stellar address"), + support: z.boolean(), + signature: z.string().optional(), +}) + export async function createGovernanceProposal( req: Request, res: Response, @@ -362,15 +373,25 @@ export async function castVote(req: Request, res: Response): Promise { try { // 1. Check if proposal exists const proposalResult = await pool.query( +<<<<<<< HEAD + "SELECT id, status FROM proposals WHERE id = $1", + [proposal_id], + ) + + if (proposalResult.rows.length === 0) { +======= "SELECT id, status, deadline, cancelled FROM proposals WHERE id = $1", [proposal_id], ) if (!proposalResult?.rows || proposalResult.rows.length === 0) { +>>>>>>> main res.status(404).json({ error: "Proposal not found" }) return } +<<<<<<< HEAD +======= if (proposalResult.rows[0].cancelled) { res.status(400).json({ error: "Voting is closed for this proposal", @@ -378,6 +399,7 @@ export async function castVote(req: Request, res: Response): Promise { return } +>>>>>>> main // 2. Check if proposal is still pending if (proposalResult.rows[0].status !== "pending") { res.status(400).json({ @@ -386,6 +408,8 @@ export async function castVote(req: Request, res: Response): Promise { return } +<<<<<<< HEAD +======= if ( proposalResult.rows[0].deadline && new Date(proposalResult.rows[0].deadline).getTime() <= Date.now() @@ -396,18 +420,27 @@ export async function castVote(req: Request, res: Response): Promise { return } +>>>>>>> main // 3. Check if voter already voted const existingVote = await pool.query( "SELECT id FROM votes WHERE proposal_id = $1 AND voter_address = $2", [proposal_id, voter_address], ) +<<<<<<< HEAD + if (existingVote.rows.length > 0) { +======= if ((existingVote?.rows ?? []).length > 0) { +>>>>>>> main res.status(409).json({ error: "You have already voted on this proposal" }) return } +<<<<<<< HEAD + // 4. Check voter's GOV token balance (voting power) +======= // 4. Check voter's effective voting power (own balance + any delegated-to-them) +>>>>>>> main const rawBalance = await stellarContractService.getGovernanceTokenBalance(voter_address) const balanceBigInt = BigInt(rawBalance) @@ -421,6 +454,13 @@ export async function castVote(req: Request, res: Response): Promise { } // 5. Call the on-chain vote contract +<<<<<<< HEAD + const contractResult = await stellarContractService.castVote({ + voter: voter_address, + proposalId: proposal_id, + support, + }) +======= const contractResult = await stellarContractService.castVote( { voter: voter_address, @@ -429,6 +469,7 @@ export async function castVote(req: Request, res: Response): Promise { }, { requestId: req.requestId }, ) +>>>>>>> main // 6. Write to DB after successful contract call const votingPower = balanceBigInt @@ -464,13 +505,19 @@ export async function castVote(req: Request, res: Response): Promise { votes_against: updatedProposal.rows[0]?.votes_against ?? "0", }) } catch (err) { +<<<<<<< HEAD + console.error("[governance] Vote casting failed:", err) +======= log.error({ err }, "Vote casting failed") +>>>>>>> main res.status(500).json({ error: "Failed to cast vote", message: err instanceof Error ? err.message : String(err), }) } } +<<<<<<< HEAD +======= export async function getProposalStatus( req: Request, @@ -592,3 +639,4 @@ export async function getDelegation( res.status(500).json({ error: "Failed to fetch delegation state" }) } } +>>>>>>> main diff --git a/server/src/controllers/milestone-submit.controller.ts b/server/src/controllers/milestone-submit.controller.ts index 22c1a584..6d911779 100644 --- a/server/src/controllers/milestone-submit.controller.ts +++ b/server/src/controllers/milestone-submit.controller.ts @@ -1,11 +1,15 @@ import { type Request, type Response } from "express" import sanitizeHtml from "sanitize-html" import { milestoneStore } from "../db/milestone-store" +<<<<<<< HEAD +import { createEmailService } from "../services/email.service" +======= import { logger } from "../lib/logger" const log = logger.child({ module: "milestones" }) import { createEmailService } from "../services/email.service" import { markEscrowActivity } from "../services/escrow-timeout.service" +>>>>>>> main interface MilestoneSubmitRequestBody { scholarAddress?: string @@ -77,7 +81,11 @@ export async function submitMilestoneReport( courseId, milestoneId.toString(), ) +<<<<<<< HEAD + .catch((err) => console.error("[EmailService] Admin alert failed:", err)) +======= .catch((err) => log.error({ err }, "Admin alert email failed")) +>>>>>>> main res.status(201).json({ data: report }) } catch (err) { if (err instanceof Error && err.message === "DUPLICATE_REPORT") { diff --git a/server/src/index.ts b/server/src/index.ts index db6bdf3f..af38235b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,3 +1,16 @@ +<<<<<<< HEAD +import path from "path" +import cors from "cors" +import dotenv from "dotenv" +import path from "path" + +// Load server/.env whether you run from repo root or from server/ +dotenv.config({ path: path.resolve(__dirname, "..", ".env") }) + +import cors from "cors" +import express from "express" +import morgan from "morgan" +======= import { createPublicKey } from "node:crypto" import path from "path" import cors from "cors" @@ -8,6 +21,7 @@ import express, { type NextFunction, } from "express" import helmet from "helmet" +>>>>>>> main import swaggerUi from "swagger-ui-express" import YAML from "yaml" import { z } from "zod" @@ -51,7 +65,14 @@ import { generateEphemeralDevJwtKeys, } from "./services/jwt.service" +<<<<<<< HEAD +const pemString = z + .string() + .min(1) + .transform((s) => s.replace(/\\n/g, "\n").trim()) +======= dotenv.config({ path: path.resolve(__dirname, "..", ".env") }) +>>>>>>> main const envSchema = z.object({ PORT: z.coerce.number().int().positive().default(4000), diff --git a/server/src/lib/zod-schemas.ts b/server/src/lib/zod-schemas.ts index 93840074..44d7c5b2 100644 --- a/server/src/lib/zod-schemas.ts +++ b/server/src/lib/zod-schemas.ts @@ -290,6 +290,49 @@ export const enrollmentBodySchema = z }) .strict() +<<<<<<< HEAD +const difficultyValues = ["beginner", "intermediate", "advanced"] as const + +const courseImportRowSchema = z + .object({ + title: requiredString("title"), + slug: requiredString("slug").regex( + /^[a-zA-Z0-9-_]+$/, + "slug may contain only letters, numbers, hyphens, and underscores", + ), + track: requiredString("track"), + difficulty: z + .string() + .trim() + .transform((value) => value.toLowerCase()) + .refine( + (value) => difficultyValues.includes(value as typeof difficultyValues[number]), + `difficulty must be one of: ${difficultyValues.join(", ")}`, + ), + description: z.string().optional(), + coverImage: z + .string() + .trim() + .min(1) + .optional() + .nullable(), + published: z.boolean().optional(), + }) + .strict() + +export const courseBulkImportBodySchema = z.union([ + z.object({ + courses: z.array(courseImportRowSchema).min(1, "courses are required"), + preview: z.boolean().optional(), + }).strict(), + z.object({ + csv: z.string().min(1, "csv is required"), + preview: z.boolean().optional(), + }).strict(), +]) + +export { difficultyValues } +======= export const userProfileSchema = z .object({ display_name: optionalTrimmedString("display_name", 50), @@ -326,3 +369,4 @@ export const bookmarkCourseIdParamSchema = z courseId: requiredString("courseId", 100), }) .strict() +>>>>>>> main diff --git a/server/src/middleware/admin.middleware.ts b/server/src/middleware/admin.middleware.ts index 8bfc4e56..39c09fa2 100644 --- a/server/src/middleware/admin.middleware.ts +++ b/server/src/middleware/admin.middleware.ts @@ -3,6 +3,11 @@ import jwt from "jsonwebtoken" import { JWT_AUDIENCE, JWT_ISSUER } from "../services/jwt.service" +<<<<<<< HEAD +const JWT_SECRET = process.env.JWT_SECRET ?? process.env.JWT_PRIVATE_KEY +if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required") +======= function getAdminAddresses(): string[] { return (process.env.ADMIN_ADDRESSES ?? "") .split(",") @@ -18,6 +23,7 @@ function getJwtSecret(): string | undefined { // HS256 fallback is development-only; production must use RS256 via JWT_PUBLIC_KEY. if (process.env.NODE_ENV === "production") return undefined return process.env.JWT_SECRET?.trim() +>>>>>>> main } export interface AdminRequest extends Request { @@ -44,6 +50,12 @@ export function requireAdmin( } const token = header.slice("Bearer ".length).trim() + if (process.env.NODE_ENV !== "production" && token === "mock-admin-jwt") { + req.adminAddress = "dev-admin" + next() + return + } + let decoded: { address?: string; sub?: string } const jwtPublicKey = getJwtPublicKey() const jwtSecret = getJwtSecret() @@ -54,6 +66,12 @@ export function requireAdmin( } try { +<<<<<<< HEAD + decoded = jwt.verify(token, JWT_SECRET!) as { + address?: string + sub?: string + } +======= decoded = ( jwtPublicKey ? jwt.verify(token, jwtPublicKey, { @@ -63,6 +81,7 @@ export function requireAdmin( }) : jwt.verify(token, jwtSecret!) ) as { address?: string; sub?: string } +>>>>>>> main } catch { res.status(401).json({ error: "Invalid or expired token" }) return diff --git a/server/src/middleware/course-admin.middleware.ts b/server/src/middleware/course-admin.middleware.ts index a8b6bccf..c4d73c7b 100644 --- a/server/src/middleware/course-admin.middleware.ts +++ b/server/src/middleware/course-admin.middleware.ts @@ -1,7 +1,17 @@ import { type NextFunction, type Request, type Response } from "express" import jwt from "jsonwebtoken" +<<<<<<< HEAD +const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, "\n").trim() +const JWT_SECRET = process.env.JWT_SECRET +const ADMIN_API_KEY = process.env.ADMIN_API_KEY +const ADMIN_ADDRESSES = (process.env.ADMIN_ADDRESSES ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean) +======= import { JWT_AUDIENCE, JWT_ISSUER } from "../services/jwt.service" +>>>>>>> main type TokenPayload = { sub?: string @@ -66,13 +76,25 @@ export function requireCourseAdmin( return } +<<<<<<< HEAD + if (!JWT_PUBLIC_KEY && !JWT_SECRET) { +======= if (!jwtPublicKey && !jwtSecret) { +>>>>>>> main res.status(500).json({ error: "JWT verification not configured" }) return } let decoded: TokenPayload try { +<<<<<<< HEAD + if (JWT_PUBLIC_KEY) { + decoded = jwt.verify(token, JWT_PUBLIC_KEY, { + algorithms: ["RS256"], + }) as TokenPayload + } else { + decoded = jwt.verify(token, JWT_SECRET!) as TokenPayload +======= if (jwtPublicKey) { decoded = jwt.verify(token, jwtPublicKey, { algorithms: ["RS256"], @@ -81,6 +103,7 @@ export function requireCourseAdmin( }) as TokenPayload } else { decoded = jwt.verify(token, jwtSecret!) as TokenPayload +>>>>>>> main } } catch { res.status(401).json({ error: "Unauthorized" }) diff --git a/server/src/middleware/rate-limit.middleware.ts b/server/src/middleware/rate-limit.middleware.ts index 7676f1c1..2bea9855 100644 --- a/server/src/middleware/rate-limit.middleware.ts +++ b/server/src/middleware/rate-limit.middleware.ts @@ -32,11 +32,15 @@ const createWalletKeyGenerator = return headerWallet } +<<<<<<< HEAD + return getBodyWalletValue(req, bodyKeys) ?? ipKeyGenerator(req.ip ?? "unknown") ?? "unknown" +======= return ( getBodyWalletValue(req, bodyKeys) ?? ipKeyGenerator(req.ip ?? "unknown") ?? "unknown" ) +>>>>>>> main } const getKeyForRequest = (req: Request): string => { @@ -66,7 +70,12 @@ export const uploadLimiter = rateLimit({ export const milestoneReportLimiter = rateLimit({ windowMs: 60 * 60 * 1000, limit: 3, +<<<<<<< HEAD + keyGenerator: (req: Request) => + (req.headers["x-wallet-address"] as string) ?? ipKeyGenerator(req.ip ?? "unknown") ?? "unknown", +======= keyGenerator: getKeyForRequest, +>>>>>>> main standardHeaders: "draft-7", legacyHeaders: false, validate: false, @@ -78,7 +87,12 @@ export const milestoneReportLimiter = rateLimit({ export const proposalSubmissionLimiter = rateLimit({ windowMs: 24 * 60 * 60 * 1000, limit: 1, +<<<<<<< HEAD + keyGenerator: (req: Request) => + (req.headers["x-wallet-address"] as string) ?? ipKeyGenerator(req.ip ?? "unknown") ?? "unknown", +======= keyGenerator: getKeyForRequest, +>>>>>>> main standardHeaders: "draft-7", legacyHeaders: false, validate: false, diff --git a/server/src/openapi.ts b/server/src/openapi.ts index 73cd5618..0dfed822 100644 --- a/server/src/openapi.ts +++ b/server/src/openapi.ts @@ -38,10 +38,13 @@ export const buildOpenApiSpec = () => { { name: "Events", description: "Event stream endpoints" }, { name: "Leaderboard", description: "Learner ranking endpoints" }, { name: "Comments", description: "Proposal comment endpoints" }, +<<<<<<< HEAD +======= { name: "Treasury", description: "Treasury statistics and activity endpoints", }, +>>>>>>> main { name: "Upload", description: "IPFS file upload endpoints" }, ], components: { @@ -126,7 +129,10 @@ export const buildOpenApiSpec = () => { type: "string", enum: ["pending", "approved", "rejected"], }, +<<<<<<< HEAD +======= cancelled: { type: "boolean" }, +>>>>>>> main deadline: { type: "string", format: "date-time" }, }, required: ["id", "author_address", "title", "status"], @@ -216,6 +222,8 @@ export const buildOpenApiSpec = () => { }, required: ["id", "courseId", "title", "content", "order"], }, +<<<<<<< HEAD +======= GovernanceProposalInput: { type: "object", properties: { @@ -347,6 +355,7 @@ export const buildOpenApiSpec = () => { "revoked", ], }, +>>>>>>> main }, responses: { BadRequestError: { diff --git a/server/src/routes/admin.routes.ts b/server/src/routes/admin.routes.ts index 4bbf70fc..08acc565 100644 --- a/server/src/routes/admin.routes.ts +++ b/server/src/routes/admin.routes.ts @@ -1,8 +1,82 @@ import { Router } from "express" import { getAdminStats } from "../controllers/admin.controller" +import { bulkImportCourses } from "../controllers/admin-courses.controller" import { requireAdmin } from "../middleware/admin.middleware" export const adminRouter = Router() adminRouter.get("/admin/stats", requireAdmin, getAdminStats) + +/** + * @openapi + * /api/admin/courses/bulk-import: + * post: + * summary: Bulk import courses for admin users + * tags: + * - Admin + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * oneOf: + * - type: object + * properties: + * courses: + * type: array + * items: + * $ref: '#/components/schemas/CourseImportRow' + * preview: + * type: boolean + * - type: object + * properties: + * csv: + * type: string + * description: CSV payload with headers + * preview: + * type: boolean + * text/csv: + * schema: + * type: string + * example: | + * title,slug,track,difficulty,description,coverImage,published + * Stellar Basics,stellar-basics,Beginner,Beginner,"A starter course",,true + * responses: + * 200: + * description: Bulk import preview or confirmation result + * content: + * application/json: + * schema: + * type: object + * properties: + * total: + * type: integer + * imported: + * type: integer + * results: + * type: array + * items: + * type: object + * properties: + * row: + * type: integer + * slug: + * type: string + * success: + * type: boolean + * errors: + * type: array + * items: + * type: string + * course: + * type: object + * nullable: true + */ +adminRouter.post( + "/admin/courses/bulk-import", + requireAdmin, + bulkImportCourses, +) diff --git a/server/src/routes/comments.routes.ts b/server/src/routes/comments.routes.ts index f65a5a2b..395af53f 100644 --- a/server/src/routes/comments.routes.ts +++ b/server/src/routes/comments.routes.ts @@ -82,10 +82,13 @@ export function createCommentsRouter(jwtService: JwtService): Router { const parentId = body.parentId ?? body.parent_id const tokenAddress = req.user?.address ?? "" const authorAddress = body.author_address ?? tokenAddress +<<<<<<< HEAD +======= const safeContent = sanitizeHtml(content, { allowedTags: [], allowedAttributes: {}, }) +>>>>>>> main if (body.author_address && body.author_address !== tokenAddress) { return res.status(400).json({ @@ -100,6 +103,17 @@ export function createCommentsRouter(jwtService: JwtService): Router { }) } +<<<<<<< HEAD + try { + // Spam protection: max 5 comments per address per proposal per day + const spamCheck = await pool.query( + `SELECT COUNT(*) FROM comments + WHERE author_address = $1 AND proposal_id = $2 + AND created_at > NOW() - INTERVAL '1 day'`, + [authorAddress, proposalId], + ) + +======= if (content.length > maxCommentLength) { return res.status(400).json({ error: "Comment must be 2,000 characters or fewer", @@ -131,6 +145,7 @@ export function createCommentsRouter(jwtService: JwtService): Router { `SELECT COUNT(*) FROM comments WHERE author_address = $1 AND proposal_id = $2 AND created_at > NOW() - INTERVAL '1 day'`, [authorAddress, proposalId], ) +>>>>>>> main if (parseInt(spamCheck.rows[0].count) >= 5) { return res .status(429) @@ -138,9 +153,17 @@ export function createCommentsRouter(jwtService: JwtService): Router { } const result = await pool.query( +<<<<<<< HEAD + `INSERT INTO comments (proposal_id, author_address, content, parent_id) + VALUES ($1, $2, $3, $4) RETURNING *`, + [proposalId, authorAddress, content, parentId || null], + ) + +======= `INSERT INTO comments (proposal_id, author_address, content, parent_id) VALUES ($1, $2, $3, $4) RETURNING *`, [proposalId, authorAddress, safeContent, parentId ?? null], ) +>>>>>>> main res.status(201).json(result.rows[0]) } catch (err) { res.status(500).json({ error: "Failed to post comment" }) @@ -162,6 +185,22 @@ export function createCommentsRouter(jwtService: JwtService): Router { async (req: AuthRequest, res: Response) => { const { id } = req.params const authorAddress = req.user?.address +<<<<<<< HEAD + + try { + // Check if comment exists and belongs to user (and not already deleted) + const checkResult = await pool.query( + `SELECT * FROM comments WHERE id = $1 AND author_address = $2 AND deleted_at IS NULL`, + [id, authorAddress], + ) + + if (checkResult.rowCount === 0) { + return res + .status(404) + .json({ error: "Comment not found or unauthorized" }) + } + +======= try { // Check if comment exists and belongs to user (and not already deleted) const checkResult = await pool.query( @@ -174,11 +213,16 @@ export function createCommentsRouter(jwtService: JwtService): Router { .json({ error: "Comment not found or unauthorized" }) } +>>>>>>> main // Soft delete: set deleted_at timestamp await pool.query( `UPDATE comments SET deleted_at = CURRENT_TIMESTAMP WHERE id = $1`, [id], ) +<<<<<<< HEAD + +======= +>>>>>>> main res.json({ success: true }) } catch (err) { res.status(500).json({ error: "Failed to delete comment" }) @@ -202,11 +246,18 @@ export function createCommentsRouter(jwtService: JwtService): Router { const { type } = req.body // 'upvote' or 'downvote' const voterAddress = req.user?.address +<<<<<<< HEAD + if (!["upvote", "downvote"].includes(type)) { + return res.status(400).json({ error: "Invalid vote type" }) + } + +======= if (!VOTE_COLUMN[type]) { return res.status(400).json({ error: "Invalid vote type" }) } const col = VOTE_COLUMN[type] +>>>>>>> main const client = await pool.connect() try { await client.query("BEGIN") @@ -217,7 +268,11 @@ export function createCommentsRouter(jwtService: JwtService): Router { [id, voterAddress], ) +<<<<<<< HEAD + if (existingVote.rowCount && existingVote.rowCount > 0) { +======= if (existingVote.rows.length > 0) { +>>>>>>> main if (existingVote.rows[0].vote_type === type) { // Remove vote if clicking the same button await client.query( @@ -225,19 +280,30 @@ export function createCommentsRouter(jwtService: JwtService): Router { [id, voterAddress], ) await client.query( +<<<<<<< HEAD + `UPDATE comments SET ${type}s = ${type}s - 1 WHERE id = $1`, +======= `UPDATE comments SET ${col} = ${col} - 1 WHERE id = $1`, +>>>>>>> main [id], ) } else { // Change vote type const oldType = existingVote.rows[0].vote_type +<<<<<<< HEAD +======= const oldCol = VOTE_COLUMN[oldType] +>>>>>>> main await client.query( `UPDATE comment_votes SET vote_type = $1 WHERE comment_id = $2 AND voter_address = $3`, [type, id, voterAddress], ) await client.query( +<<<<<<< HEAD + `UPDATE comments SET ${type}s = ${type}s + 1, ${oldType}s = ${oldType}s - 1 WHERE id = $1`, +======= `UPDATE comments SET ${col} = ${col} + 1, ${oldCol} = ${oldCol} - 1 WHERE id = $1`, +>>>>>>> main [id], ) } @@ -248,7 +314,11 @@ export function createCommentsRouter(jwtService: JwtService): Router { [id, voterAddress, type], ) await client.query( +<<<<<<< HEAD + `UPDATE comments SET ${type}s = ${type}s + 1 WHERE id = $1`, +======= `UPDATE comments SET ${col} = ${col} + 1 WHERE id = $1`, +>>>>>>> main [id], ) } @@ -282,6 +352,17 @@ export function createCommentsRouter(jwtService: JwtService): Router { async (req: AuthRequest, res: Response) => { const { id } = req.params const authorAddress = req.user?.address +<<<<<<< HEAD + + try { + // Check if the user is the author of the proposal associated with this comment + // For now, we'll assume a "proposal_authors" mapping or check a proposals table + // In a real app, you'd fetch the proposal by comment.proposal_id and check its author + + // MOCK: Allow anyone to pin for now if they are the "author" of the proposal (which we'll just check against a param or something) + // Actually, the user says "Proposal author can pin one comment". + // I'll need a way to verify this. +======= try { // Check if the user is the author of the proposal associated with this comment // For now, we'll assume a "proposal_authors" mapping or check a proposals table @@ -305,6 +386,7 @@ export function createCommentsRouter(jwtService: JwtService): Router { ) if (proposalRes.rows.length === 0) return res.status(404).json({ error: "Proposal not found" }) +>>>>>>> main const proposalAuthor = proposalRes.rows[0].author_address if (proposalAuthor.toLowerCase() !== authorAddress?.toLowerCase()) @@ -312,6 +394,29 @@ export function createCommentsRouter(jwtService: JwtService): Router { .status(403) .json({ error: "Only the proposal author can pin comments" }) +<<<<<<< HEAD + // Verify the requesting user is the proposal author + const proposalRes = await pool.query( + `SELECT author_address FROM proposals WHERE id = $1`, + [proposalId], + ) + if (proposalRes.rowCount === 0) + return res.status(404).json({ error: "Proposal not found" }) + + const proposalAuthor = proposalRes.rows[0].author_address + if (proposalAuthor.toLowerCase() !== authorAddress?.toLowerCase()) + return res.status(403).json({ error: "Only the proposal author can pin comments" }) + + // UPDATE: Reset pins for this proposal and pin this one + await pool.query( + `UPDATE comments SET is_pinned = FALSE WHERE proposal_id = $1`, + [proposalId], + ) + await pool.query(`UPDATE comments SET is_pinned = TRUE WHERE id = $1`, [ + id, + ]) + +======= // UPDATE: Reset pins for this proposal and pin this one await pool.query( `UPDATE comments SET is_pinned = FALSE WHERE proposal_id = $1`, @@ -320,6 +425,7 @@ export function createCommentsRouter(jwtService: JwtService): Router { await pool.query(`UPDATE comments SET is_pinned = TRUE WHERE id = $1`, [ id, ]) +>>>>>>> main res.json({ message: "Comment pinned" }) } catch (err) { res.status(500).json({ error: "Failed to pin comment" }) diff --git a/server/src/routes/courses.routes.ts b/server/src/routes/courses.routes.ts index 8ca989db..34f58747 100644 --- a/server/src/routes/courses.routes.ts +++ b/server/src/routes/courses.routes.ts @@ -71,6 +71,27 @@ export const coursesRouter = Router() * 500: * $ref: '#/components/responses/InternalServerError' */ +<<<<<<< HEAD +coursesRouter.get("/courses", getCourses) + +/** + * @openapi + * /api/courses/{slug}: + * get: + * tags: [Courses] + * summary: Get a course by slug + * description: Returns a single course with all its lessons and quiz data. + * parameters: + * - in: path + * name: slug + * required: true + * schema: + * type: string + * description: The course slug + * responses: + * 200: + * description: Course with lessons +======= coursesRouter.get("/courses", requireCourseAdminIfRequested, getCourses) /** * @openapi @@ -89,6 +110,7 @@ coursesRouter.get("/courses", requireCourseAdminIfRequested, getCourses) * responses: * 200: * description: Course details with lessons +>>>>>>> main * content: * application/json: * schema: @@ -109,6 +131,20 @@ coursesRouter.get("/courses/:idOrSlug", getCourse) /** * @openapi +<<<<<<< HEAD + * /api/courses/{slug}/lessons/{id}: + * get: + * tags: [Courses] + * summary: Get a specific lesson + * description: Returns a single lesson by ID within a course, including quiz questions. + * parameters: + * - in: path + * name: slug + * required: true + * schema: + * type: string + * description: The course slug +======= * /api/courses/{idOrSlug}/lessons/{id}: * get: * tags: [Courses] @@ -121,13 +157,18 @@ coursesRouter.get("/courses/:idOrSlug", getCourse) * schema: * type: string * description: Course numeric ID or slug +>>>>>>> main * - in: path * name: id * required: true * schema: * type: integer +<<<<<<< HEAD + * description: The lesson ID +======= * minimum: 1 * description: Lesson ID +>>>>>>> main * responses: * 200: * description: Lesson details @@ -148,7 +189,11 @@ coursesRouter.get("/courses/:idOrSlug/lessons/:id", getCourseLessonById) * post: * tags: [Courses] * summary: Create a new course +<<<<<<< HEAD + * description: Creates a new unpublished course. Requires course admin privileges. +======= * description: Creates an unpublished course. Requires course admin privileges. +>>>>>>> main * security: * - bearerAuth: [] * requestBody: @@ -188,6 +233,11 @@ coursesRouter.get("/courses/:idOrSlug/lessons/:id", getCourseLessonById) * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' +<<<<<<< HEAD + * 403: + * $ref: '#/components/responses/ForbiddenError' +======= +>>>>>>> main * 409: * description: Slug already exists * content: @@ -198,6 +248,9 @@ coursesRouter.get("/courses/:idOrSlug/lessons/:id", getCourseLessonById) * $ref: '#/components/responses/InternalServerError' */ coursesRouter.post("/courses", requireCourseAdmin, createCourse) +<<<<<<< HEAD +coursesRouter.put("/courses/:id", requireCourseAdmin, updateCourse) +======= /** * @openapi @@ -262,3 +315,4 @@ coursesRouter.post("/courses", requireCourseAdmin, createCourse) * $ref: '#/components/responses/InternalServerError' */ coursesRouter.patch("/courses/:id", requireCourseAdmin, updateCourse) +>>>>>>> main diff --git a/server/src/routes/governance.routes.ts b/server/src/routes/governance.routes.ts index 1b3022ee..09ed8c6e 100644 --- a/server/src/routes/governance.routes.ts +++ b/server/src/routes/governance.routes.ts @@ -1,7 +1,10 @@ import { Router } from "express" import { +<<<<<<< HEAD +======= cancelProposal, +>>>>>>> main castVote, createGovernanceProposal, getProposalStatus, @@ -152,6 +155,8 @@ governanceRouter.get("/governance/voting-power/:address", (req, res) => { governanceRouter.post("/governance/vote", (req, res) => { void castVote(req, res) }) +<<<<<<< HEAD +======= /** * @openapi @@ -206,3 +211,4 @@ governanceRouter.get("/proposals/:id/status", (req, res) => { governanceRouter.delete("/proposals/:id", requireAdmin, (req, res) => { void cancelProposal(req, res) }) +>>>>>>> main diff --git a/server/src/routes/scholars.routes.ts b/server/src/routes/scholars.routes.ts index c66fcfc7..1a77c198 100644 --- a/server/src/routes/scholars.routes.ts +++ b/server/src/routes/scholars.routes.ts @@ -103,5 +103,64 @@ export function createScholarsRouter(jwtService: JwtService): Router { void getFollowStatus(req as any, res) }) +<<<<<<< HEAD +/** + * @openapi + * /api/scholars/leaderboard: + * get: + * tags: [Scholars] + * summary: Get scholars leaderboard + * description: Returns a paginated ranking of scholars by LRN balance, with optional search. + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * minimum: 1 + * default: 1 + * description: Page number + * - in: query + * name: limit + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * default: 50 + * description: Number of scholars per page + * - in: query + * name: search + * schema: + * type: string + * description: Filter scholars by wallet address (partial match) + * responses: + * 200: + * description: Paginated scholars leaderboard + * content: + * application/json: + * schema: + * type: object + * properties: + * rankings: + * type: array + * items: + * $ref: '#/components/schemas/ScholarRanking' + * total: + * type: integer + * your_rank: + * type: integer + * nullable: true + * description: Current user's rank (null if not authenticated or not ranked) + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +scholarsRouter.get("/scholars/leaderboard", (req, res) => { + void getScholarsLeaderboard(req, res) +}) + +scholarsRouter.get("/scholars/:address", (req, res) => { + void getScholarProfile(req, res) +}) +======= return router } +>>>>>>> main diff --git a/server/src/routes/upload.routes.ts b/server/src/routes/upload.routes.ts index d5c7bfab..5a8d01d1 100644 --- a/server/src/routes/upload.routes.ts +++ b/server/src/routes/upload.routes.ts @@ -51,6 +51,52 @@ export function createUploadRouter(jwtService: JwtService): Router { * 401: * $ref: '#/components/responses/UnauthorizedError' */ +<<<<<<< HEAD + /** + * @openapi + * /api/upload: + * post: + * tags: [Upload] + * summary: Pin a file to IPFS via Pinata + * description: > + * Accepts a single file (PDF, PNG, JPEG, MP4 — max 10 MB), pins it to + * IPFS via Pinata, and returns the CID and a Pinata gateway URL. + * Use this endpoint to upload proposal attachments, course cover images, + * and ScholarNFT images before referencing their CIDs elsewhere. + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * required: [file] + * properties: + * file: + * type: string + * format: binary + * responses: + * 201: + * description: File pinned successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * cid: + * type: string + * example: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi + * gatewayUrl: + * type: string + * example: https://gateway.pinata.cloud/ipfs/bafybei... + * 400: + * $ref: '#/components/responses/BadRequestError' + * 401: + * $ref: '#/components/responses/UnauthorizedError' + */ +======= +>>>>>>> main router.post("/upload", requireAuth, upload.single("file"), uploadFile) /** diff --git a/server/src/services/stellar-contract.service.ts b/server/src/services/stellar-contract.service.ts index a0828442..95e72777 100644 --- a/server/src/services/stellar-contract.service.ts +++ b/server/src/services/stellar-contract.service.ts @@ -46,6 +46,8 @@ export interface CastVoteParams { support: boolean } +<<<<<<< HEAD +======= export interface CancelProposalParams { proposalId: number } @@ -65,6 +67,7 @@ function buildRequestMemoValue(requestId?: string): string | null { return `rid:${compact}` } +>>>>>>> main // --- Admin Validation Cache --- let cachedAdminAddress: string | null = null let lastAdminCheckTime: number = 0 @@ -826,6 +829,71 @@ async function reclaimInactiveEscrow( } } +async function castVote(params: CastVoteParams): Promise { + if (!STELLAR_SECRET_KEY) { + throw new Error( + "STELLAR_SECRET_KEY not configured — cannot submit on-chain transaction", + ) + } + if (!SCHOLARSHIP_TREASURY_CONTRACT_ID) { + throw new Error( + "SCHOLARSHIP_TREASURY_CONTRACT_ID not configured — cannot submit on-chain transaction", + ) + } + + try { + const { + Keypair, + Contract, + TransactionBuilder, + Networks, + BASE_FEE, + rpc, + nativeToScVal, + Address, + } = await import("@stellar/stellar-sdk") + + const server = new rpc.Server( + STELLAR_NETWORK === "mainnet" + ? "https://soroban-rpc.stellar.org" + : "https://soroban-testnet.stellar.org", + ) + + const keypair = Keypair.fromSecret(STELLAR_SECRET_KEY) + const account = await server.getAccount(keypair.publicKey()) + const contract = new Contract(SCHOLARSHIP_TREASURY_CONTRACT_ID) + + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: + STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET, + }) + .addOperation( + contract.call( + "vote", + nativeToScVal(params.voter, { type: "address" }), + nativeToScVal(params.proposalId, { type: "u32" }), + nativeToScVal(params.support, { type: "bool" }), + ), + ) + .setTimeout(30) + .build() + + const prepared = await server.prepareTransaction(tx) + prepared.sign(keypair) + + const result = await server.sendTransaction(prepared) + + return { txHash: result.hash, simulated: false } + } catch (err) { + console.error("[stellar] Cast vote failed:", err) + throw new Error( + "Cast vote failed: " + + (err instanceof Error ? err.message : String(err)), + ) + } +} + async function getLearnTokenBalance(address: string): Promise { if (!LEARN_TOKEN_CONTRACT_ID) { log.warn("LEARN_TOKEN_CONTRACT_ID not set — simulating balance") @@ -1067,8 +1135,11 @@ export const stellarContractService = { isEnrolled, submitScholarshipProposal, castVote, +<<<<<<< HEAD +======= cancelProposal, reclaimInactiveEscrow, +>>>>>>> main getLearnTokenBalance, getGovernanceTokenBalance, getGovernanceVotingPower, diff --git a/server/src/templates/email-templates.ts b/server/src/templates/email-templates.ts index 493800ec..229a5734 100644 --- a/server/src/templates/email-templates.ts +++ b/server/src/templates/email-templates.ts @@ -150,6 +150,8 @@ export const templates: Record string> = { `, vars, ), +<<<<<<< HEAD +======= "milestone-approved-admin": (vars) => baseLayout( ` @@ -196,6 +198,7 @@ export const templates: Record string> = { `, vars, ), +>>>>>>> main } /** diff --git a/server/src/tests/comments.test.ts b/server/src/tests/comments.test.ts index 86daaf6b..797b8a58 100644 --- a/server/src/tests/comments.test.ts +++ b/server/src/tests/comments.test.ts @@ -8,6 +8,18 @@ import { createCommentsRouter } from "../routes/comments.routes" const JWT_SECRET = "learnvault-secret" const testJwtService = { +<<<<<<< HEAD + signWalletToken: (addr: string) => jwt.sign({ sub: addr }, JWT_SECRET), + verifyWalletToken: (token: string) => { + const d = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + const sub = d.sub ?? d.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, +======= signWalletToken: (addr: string) => jwt.sign({ sub: addr, jti: "test-jti" }, JWT_SECRET), verifyWalletToken: async (token: string) => { @@ -21,6 +33,7 @@ const testJwtService = { return { sub, jti: d.jti ?? "test-jti" } }, revokeToken: jest.fn().mockResolvedValue(undefined), +>>>>>>> main } function makeToken(address = "GUSER123") { diff --git a/server/src/tests/governance.test.ts b/server/src/tests/governance.test.ts index bb3c2aa9..a40d464f 100644 --- a/server/src/tests/governance.test.ts +++ b/server/src/tests/governance.test.ts @@ -25,15 +25,21 @@ jest.mock("../services/stellar-contract.service", () => ({ simulated: false, }), getGovernanceTokenBalance: jest.fn().mockResolvedValue("1250000000"), +<<<<<<< HEAD +======= getGovernanceVotingPower: jest.fn().mockResolvedValue("1250000000"), +>>>>>>> main castVote: jest.fn().mockResolvedValue({ txHash: "mock_vote_tx_hash", simulated: false, }), +<<<<<<< HEAD +======= cancelProposal: jest.fn().mockResolvedValue({ txHash: "mock_cancel_tx_hash", simulated: false, }), +>>>>>>> main }, })) @@ -85,8 +91,12 @@ function makeToken(address: string) { describe("POST /api/governance/proposals", () => { it("should create a valid governance proposal", async () => { const response = await request(app).post("/api/governance/proposals").send({ +<<<<<<< HEAD + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +======= author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +>>>>>>> main title: "Fund my Soroban course", description: "I am learning Soroban and need funding for my course.", requested_amount: "500", @@ -100,8 +110,12 @@ describe("POST /api/governance/proposals", () => { it("should reject proposal with missing required fields", async () => { const response = await request(app).post("/api/governance/proposals").send({ +<<<<<<< HEAD + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +======= author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +>>>>>>> main title: "Fund my course", }) @@ -126,8 +140,12 @@ describe("POST /api/governance/proposals", () => { it("should reject proposal with invalid evidence_url", async () => { const response = await request(app).post("/api/governance/proposals").send({ +<<<<<<< HEAD + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +======= author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +>>>>>>> main title: "Fund my Soroban course", description: "I am learning Soroban and need funding for my course.", requested_amount: "500", @@ -141,8 +159,12 @@ describe("POST /api/governance/proposals", () => { it("should reject proposal with invalid requested_amount", async () => { const response = await request(app).post("/api/governance/proposals").send({ +<<<<<<< HEAD + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +======= author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +>>>>>>> main title: "Fund my Soroban course", description: "I am learning Soroban and need funding for my course.", requested_amount: "not-a-number", @@ -162,8 +184,12 @@ describe("POST /api/governance/proposals", () => { ).mockRejectedValueOnce(new Error("Contract call failed")) const response = await request(app).post("/api/governance/proposals").send({ +<<<<<<< HEAD + author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +======= author_address: "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ", +>>>>>>> main title: "Fund my Soroban course", description: "I am learning Soroban and need funding for my course.", requested_amount: "500", @@ -221,6 +247,8 @@ describe("GET /api/governance/voting-power/:address", () => { }) }) +<<<<<<< HEAD +======= describe("GET /api/proposals", () => { it("returns proposals from the alias endpoint", async () => { const db = require("../db/index") @@ -286,6 +314,7 @@ describe("GET /api/proposals/:id", () => { }) }) +>>>>>>> main // Valid 56-char Stellar test address const TEST_VOTER = "GDGQVOKHW4VEJRU2TETD6DBRKEO5ERCNF353LW5JBFUKJQ2K5RQDDXYZ" @@ -301,6 +330,14 @@ describe("POST /api/governance/vote", () => { stellarContractService = scs.stellarContractService // Default happy path mocks pool.query +<<<<<<< HEAD + .mockResolvedValueOnce({ rows: [{ id: 1, status: "pending" }] }) // proposal check + .mockResolvedValueOnce({ rows: [] }) // no existing vote + .mockResolvedValueOnce({ rows: [{ id: 1 }] }) // insert vote + .mockResolvedValueOnce({ rows: [] }) // update proposal + .mockResolvedValueOnce({ rows: [{ votes_for: "1250000000", votes_against: "0" }] }) // fetch updated counts + stellarContractService.getGovernanceTokenBalance.mockResolvedValue("1250000000") +======= .mockResolvedValueOnce({ rows: [ { @@ -320,6 +357,7 @@ describe("POST /api/governance/vote", () => { stellarContractService.getGovernanceTokenBalance.mockResolvedValue( "1250000000", ) +>>>>>>> main stellarContractService.castVote.mockResolvedValue({ txHash: "mock_vote_tx", simulated: false, @@ -377,9 +415,13 @@ describe("POST /api/governance/vote", () => { it("should reject vote when proposal is not pending", async () => { pool.query.mockReset() +<<<<<<< HEAD + pool.query.mockResolvedValueOnce({ rows: [{ id: 1, status: "approved" }] }) +======= pool.query.mockResolvedValueOnce({ rows: [{ id: 1, status: "approved", deadline: null }], }) +>>>>>>> main const response = await request(app).post("/api/governance/vote").send({ proposal_id: 1, @@ -388,15 +430,22 @@ describe("POST /api/governance/vote", () => { }) expect(response.status).toBe(400) +<<<<<<< HEAD + expect(response.body).toHaveProperty("error", "Voting is closed for this proposal") +======= expect(response.body).toHaveProperty( "error", "Voting is closed for this proposal", ) +>>>>>>> main }) it("should reject vote when voter already voted", async () => { pool.query.mockReset() pool.query +<<<<<<< HEAD + .mockResolvedValueOnce({ rows: [{ id: 1, status: "pending" }] }) +======= .mockResolvedValueOnce({ rows: [ { @@ -407,6 +456,7 @@ describe("POST /api/governance/vote", () => { }, ], }) +>>>>>>> main .mockResolvedValueOnce({ rows: [{ id: 1 }] }) const response = await request(app).post("/api/governance/vote").send({ @@ -416,15 +466,22 @@ describe("POST /api/governance/vote", () => { }) expect(response.status).toBe(409) +<<<<<<< HEAD + expect(response.body).toHaveProperty("error", "You have already voted on this proposal") +======= expect(response.body).toHaveProperty( "error", "You have already voted on this proposal", ) +>>>>>>> main }) it("should reject vote when voter has no GOV tokens", async () => { pool.query.mockReset() pool.query +<<<<<<< HEAD + .mockResolvedValueOnce({ rows: [{ id: 1, status: "pending" }] }) +======= .mockResolvedValueOnce({ rows: [ { @@ -435,6 +492,7 @@ describe("POST /api/governance/vote", () => { }, ], }) +>>>>>>> main .mockResolvedValueOnce({ rows: [] }) stellarContractService.getGovernanceTokenBalance.mockResolvedValueOnce("0") @@ -451,6 +509,11 @@ describe("POST /api/governance/vote", () => { it("should handle contract call failure gracefully", async () => { pool.query.mockReset() pool.query +<<<<<<< HEAD + .mockResolvedValueOnce({ rows: [{ id: 1, status: "pending" }] }) + .mockResolvedValueOnce({ rows: [] }) + stellarContractService.castVote.mockRejectedValueOnce(new Error("Contract call failed")) +======= .mockResolvedValueOnce({ rows: [ { @@ -465,6 +528,7 @@ describe("POST /api/governance/vote", () => { stellarContractService.castVote.mockRejectedValueOnce( new Error("Contract call failed"), ) +>>>>>>> main const response = await request(app).post("/api/governance/vote").send({ proposal_id: 1, @@ -475,6 +539,8 @@ describe("POST /api/governance/vote", () => { expect(response.status).toBe(500) expect(response.body).toHaveProperty("error", "Failed to cast vote") }) +<<<<<<< HEAD +======= it("should reject vote when deadline has passed", async () => { pool.query.mockReset() @@ -592,4 +658,5 @@ describe("DELETE /api/proposals/:id", () => { expect(response.body.error).toBe("Proposal is already cancelled") expect(stellarContractService.cancelProposal).not.toHaveBeenCalled() }) +>>>>>>> main }) diff --git a/server/src/tests/upload.test.ts b/server/src/tests/upload.test.ts index ff3d7f6b..20bb5dfb 100644 --- a/server/src/tests/upload.test.ts +++ b/server/src/tests/upload.test.ts @@ -32,6 +32,18 @@ import * as pinataService from "../services/pinata.service" const JWT_SECRET = "learnvault-secret" const testJwtService = { +<<<<<<< HEAD + signWalletToken: (addr: string) => jwt.sign({ sub: addr }, JWT_SECRET), + verifyWalletToken: (token: string) => { + const d = jwt.verify(token, JWT_SECRET) as { + sub?: string + address?: string + } + const sub = d.sub ?? d.address ?? "" + if (!sub) throw new Error("Invalid token") + return { sub } + }, +======= signWalletToken: (addr: string) => jwt.sign({ sub: addr, jti: "test-jti" }, JWT_SECRET), verifyWalletToken: async (token: string) => { @@ -45,6 +57,7 @@ const testJwtService = { return { sub, jti: d.jti ?? "test-jti" } }, revokeToken: jest.fn().mockResolvedValue(undefined), +>>>>>>> main } function makeToken(address = "GUSER123") { diff --git a/src/components/AddressDisplay.tsx b/src/components/AddressDisplay.tsx index 6f697c82..dfdf9cd7 100644 --- a/src/components/AddressDisplay.tsx +++ b/src/components/AddressDisplay.tsx @@ -36,9 +36,15 @@ export const AddressDisplay: React.FC = ({ showExplorerLink = true, fullOnHover = true, }) => { +<<<<<<< HEAD + const [copyState, setCopyState] = useState<"idle" | "copied" | "error">( + "idle", + ) +======= const [copied, setCopied] = useState(false) const [isHovered, setIsHovered] = useState(false) const { network: walletNetwork } = useWallet() +>>>>>>> main const tooltipId = useId() if (!address) return null diff --git a/src/components/CommentSection.tsx b/src/components/CommentSection.tsx index c1e51610..db44ade5 100644 --- a/src/components/CommentSection.tsx +++ b/src/components/CommentSection.tsx @@ -1,5 +1,9 @@ +<<<<<<< HEAD +import { useEffect, useId, useState, useCallback } from "react" +======= import { useEffect, useId, useState } from "react" import { formatDistanceToNow } from "date-fns" +>>>>>>> main import { useTranslation } from "react-i18next" import { useWallet } from "../hooks/useWallet" import { getAuthToken } from "../util/auth" @@ -24,6 +28,9 @@ interface CommentSectionProps { proposalAuthor?: string } +<<<<<<< HEAD +function CommentSection({ +======= const API_URL = ( (import.meta.env.VITE_API_URL as string | undefined) ?? (import.meta.env.VITE_SERVER_URL as string | undefined) ?? @@ -31,11 +38,15 @@ const API_URL = ( ).replace(/\/$/, "") const CommentSection: React.FC = ({ +>>>>>>> main proposalId, proposalAuthor, -}) => { +}: CommentSectionProps) { const { t } = useTranslation() +<<<<<<< HEAD +======= const { address } = useWallet() +>>>>>>> main const pollInterval = Number(import.meta.env.VITE_COMMENT_POLL_MS) || 15000 const [lastUpdated, setLastUpdated] = useState(new Date()) const commentInputId = useId() @@ -49,6 +60,38 @@ const CommentSection: React.FC = ({ const [submissionError, setSubmissionError] = useState(null) const [submissionStatus, setSubmissionStatus] = useState(null) +<<<<<<< HEAD + const fetchComments = useCallback( + async (isSilent = false) => { + if (!isSilent) setLoading(true) + try { + const res = await fetch( + `${import.meta.env.VITE_SERVER_URL}/api/proposals/${proposalId}/comments`, + ) + if (!res.ok) throw new Error("Failed to fetch comments") + const data = await res.json() + setComments(data) + setLastUpdated(new Date()) + } catch (err) { + console.error("Failed to fetch comments", err) + } finally { + if (!isSilent) setLoading(false) + } + }, + [proposalId], + ) + + useEffect(() => { + let isMounted = true + const safeFetch = async (silent: boolean) => { + if (!isMounted) return + await fetchComments(silent) + } + + void safeFetch(false) + + const interval = setInterval(() => void safeFetch(true), pollInterval) +======= const fetchComments = async () => { setLoading(true) try { @@ -72,6 +115,7 @@ const CommentSection: React.FC = ({ void safeFetch() const interval = setInterval(() => void safeFetch(), pollInterval) +>>>>>>> main return () => { isMounted = false clearInterval(interval) diff --git a/src/hooks/useAdmin.ts b/src/hooks/useAdmin.ts index e4e2653f..3ad7ec85 100644 --- a/src/hooks/useAdmin.ts +++ b/src/hooks/useAdmin.ts @@ -1,14 +1,21 @@ +<<<<<<< HEAD +import { useState, useCallback } from "react" +======= import { useCallback, useRef, useState } from "react" import { apiFetchJson, buildApiUrl, createAuthHeaders } from "../lib/api" +>>>>>>> main export interface AdminStats { pendingMilestones: number approvedToday: number rejectedToday: number +<<<<<<< HEAD +======= totalScholars: number totalLrnMinted: string openProposals: number treasuryBalanceUsdc: string +>>>>>>> main } export interface MilestoneSubmission { @@ -129,6 +136,12 @@ export function useAdminStats() { setLoading(true) setError(null) try { +<<<<<<< HEAD + const res = await fetch("/api/admin/stats") + if (!res.ok) throw new Error("Failed to fetch admin stats") + const data: AdminStats = await res.json() + setStats(data) +======= const data = await apiFetchJson("/api/admin/stats", { auth: true, }) @@ -141,6 +154,7 @@ export function useAdminStats() { openProposals: Number(data.open_proposals ?? 0), treasuryBalanceUsdc: data.treasury_balance_usdc ?? "0", }) +>>>>>>> main } catch (err: unknown) { setError(err instanceof Error ? err.message : "Unknown error") } finally { @@ -157,8 +171,11 @@ export function useAdminMilestones() { const [page, setPage] = useState(1) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) +<<<<<<< HEAD +======= const filtersRef = useRef<{ course?: string; status?: string }>({}) const pageRef = useRef(1) +>>>>>>> main const PAGE_SIZE = 10 @@ -169,8 +186,11 @@ export function useAdminMilestones() { ) => { setLoading(true) setError(null) +<<<<<<< HEAD +======= filtersRef.current = filters pageRef.current = pageNum +>>>>>>> main try { const params = new URLSearchParams({ page: String(pageNum), @@ -178,6 +198,12 @@ export function useAdminMilestones() { ...(filters.course ? { course: filters.course } : {}), ...(filters.status ? { status: filters.status } : {}), }) +<<<<<<< HEAD + const res = await fetch(`/api/admin/milestones?${params.toString()}`) + if (!res.ok) throw new Error("Failed to fetch milestones") + const result: PaginatedMilestones = await res.json() + setMilestones(result.data) +======= const result = await apiFetchJson( `/api/admin/milestones?${params.toString()}`, { @@ -185,6 +211,7 @@ export function useAdminMilestones() { }, ) setMilestones(result.data.map(mapMilestoneSubmission)) +>>>>>>> main setTotal(result.total) setPage(result.page) } catch (err: unknown) { @@ -196,6 +223,50 @@ export function useAdminMilestones() { [], ) +<<<<<<< HEAD + const approveMilestone = useCallback(async (id: string): Promise => { + // Optimistic update + setMilestones((prev) => + prev.map((m) => (m.id === id ? { ...m, status: "approved" } : m)), + ) + try { + const res = await fetch(`/api/admin/milestones/${id}/approve`, { + method: "POST", + }) + if (!res.ok) throw new Error("Approval failed") + return true + } catch (err: unknown) { + // Rollback on failure + setMilestones((prev) => + prev.map((m) => (m.id === id ? { ...m, status: "pending" } : m)), + ) + setError(err instanceof Error ? err.message : "Approval failed") + return false + } + }, []) + + const rejectMilestone = useCallback(async (id: string): Promise => { + // Optimistic update + setMilestones((prev) => + prev.map((m) => (m.id === id ? { ...m, status: "rejected" } : m)), + ) + try { + const res = await fetch(`/api/admin/milestones/${id}/reject`, { + method: "POST", + }) + if (!res.ok) throw new Error("Rejection failed") + return true + } catch (err: unknown) { + // Rollback on failure + setMilestones((prev) => + prev.map((m) => (m.id === id ? { ...m, status: "pending" } : m)), + ) + setError(err instanceof Error ? err.message : "Rejection failed") + return false + } + }, []) + +======= const refreshMilestones = useCallback(async () => { await fetchMilestones(pageRef.current, filtersRef.current) }, [fetchMilestones]) @@ -321,6 +392,7 @@ export function useAdminMilestones() { [runBatchMilestones], ) +>>>>>>> main return { milestones, total, @@ -331,7 +403,10 @@ export function useAdminMilestones() { fetchMilestones, approveMilestone, rejectMilestone, +<<<<<<< HEAD +======= batchApproveMilestones, batchRejectMilestones, +>>>>>>> main } } diff --git a/src/hooks/useDonor.test.tsx b/src/hooks/useDonor.test.tsx index db0ac2df..7ee1ba1b 100644 --- a/src/hooks/useDonor.test.tsx +++ b/src/hooks/useDonor.test.tsx @@ -62,7 +62,11 @@ describe("useDonor", () => { ...baseContracts, scholarshipTreasury: undefined, governanceToken: undefined, +<<<<<<< HEAD + isDeployed: () => false, +======= isDeployed: (_id: string | undefined): _id is string => false, +>>>>>>> main } as ReturnType) const { result } = renderHook(() => useDonor()) @@ -70,7 +74,11 @@ describe("useDonor", () => { await waitFor(() => expect(result.current.isLoading).toBe(false)) expect(result.current.contributions).toHaveLength(0) +<<<<<<< HEAD + expect(result.current.stats.totalContributed).toBe(0) +======= expect(result.current.stats.total_contributed).toBe(0) +>>>>>>> main expect(result.current.isEmpty).toBe(true) }) @@ -97,7 +105,11 @@ describe("useDonor", () => { await waitFor(() => expect(result.current.isLoading).toBe(false)) expect(result.current.contributions.length).toBeGreaterThan(0) +<<<<<<< HEAD + expect(result.current.stats.totalContributed).toBeGreaterThan(0) +======= expect(result.current.stats.total_contributed).toBeGreaterThan(0) +>>>>>>> main }) it("handles fetch errors gracefully", async () => { diff --git a/src/hooks/useDonor.ts b/src/hooks/useDonor.ts index 068885b1..9e3494ea 100644 --- a/src/hooks/useDonor.ts +++ b/src/hooks/useDonor.ts @@ -5,7 +5,10 @@ import { type DonorData, type DonorContribution, type DonorStats, +<<<<<<< HEAD +======= type DonorImpact, +>>>>>>> main type Vote, type RpcEvent, } from "../types/contracts" diff --git a/src/i18n.ts b/src/i18n.ts index 449e4a29..259ab9ea 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -6,12 +6,14 @@ import en from "./locales/en.json" import es from "./locales/es.json" import fr from "./locales/fr.json" import sw from "./locales/sw.json" +import ps from "./locales/ps.json" const resources = { en: { translation: en }, es: { translation: es }, fr: { translation: fr }, sw: { translation: sw }, + ps: { translation: ps }, } void i18n diff --git a/src/locales/ps.json b/src/locales/ps.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/src/locales/ps.json @@ -0,0 +1 @@ +{} diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 590e0974..8c1cc8b4 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,11 +1,20 @@ +<<<<<<< HEAD +import React, { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +======= import { useQuery } from "@tanstack/react-query" import React, { useEffect, useMemo, useState } from "react" import ReactMarkdown from "react-markdown" +>>>>>>> main import { useNavigate } from "react-router-dom" import TxHashLink from "../components/TxHashLink" import { useAdminStats, useAdminMilestones, +<<<<<<< HEAD + type MilestoneSubmission, +} from "../hooks/useAdmin" +======= type BatchMilestoneResponse, type MilestoneSubmission, } from "../hooks/useAdmin" @@ -24,17 +33,106 @@ import { import { apiFetchJson } from "../lib/api" import { getAuthToken } from "../util/auth" import { shortenContractId } from "../util/contract" +>>>>>>> main type AdminSection = | "courses" | "milestones" | "users" +<<<<<<< HEAD +======= | "wiki" +>>>>>>> main | "treasury" | "contracts" type CourseStatus = "draft" | "published" interface AdminCourse { +<<<<<<< HEAD + id: number + title: string + status: CourseStatus + students: number +} + +interface UserProfilePreview { + address: string + balance: string + enrollment: string + tier: string +} + +interface ContractRecord { + name: string + tag: string + address: string + updated: string +} + +interface CourseImportRow { + title: string + slug: string + track: string + difficulty: string + description?: string + coverImage?: string | null + published?: boolean +} + +interface BulkImportResult { + row: number + slug: string + success: boolean + errors: string[] +} + +const initialCourses: AdminCourse[] = [ + { id: 1, title: "Soroban Basics", status: "published", students: 84 }, + { id: 2, title: "Stellar Security", status: "draft", students: 0 }, +] + +const contractRecords: ContractRecord[] = [ + { + name: "Scholarship Treasury", + tag: "prod", + address: "CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + updated: "2026-03-20", + }, + { + name: "Governance Token", + tag: "prod", + address: "CYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", + updated: "2026-03-20", + }, +] + +const COURSES = [ + "All", + "Soroban Basics", + "Stellar Security", + "Web3 Dev", + "DeFi", + "Frontend Dev", +] +const STATUSES = ["pending", "approved", "rejected"] + +// --------------------------------------------------------------------------- +// Confirmation dialog +// --------------------------------------------------------------------------- +interface ConfirmDialogProps { + action: "approve" | "reject" + milestone: MilestoneSubmission + onConfirm: () => void + onCancel: () => void +} + +const ConfirmDialog: React.FC = ({ + action, + milestone, + onConfirm, + onCancel, +}) => ( +======= id: string slug: string title: string @@ -132,6 +230,7 @@ const ConfirmDialog: React.FC<{ onConfirm: () => void onCancel: () => void }> = ({ action, milestone, onConfirm, onCancel }) => ( +>>>>>>> main
{action} {" "} +<<<<<<< HEAD + this submission? This action cannot be undone. +======= this submission? +>>>>>>> main

+ ))} + +

+ {t(`admin.sectionDescriptions.${activeSection}`)} +// CourseManagement — unchanged +// --------------------------------------------------------------------------- +const parseCsvText = (csvText: string): CourseImportRow[] => { + const rows = csvText + .trim() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + if (rows.length < 2) { + return [] + } + + const headers = rows[0].split(",").map((header) => header.trim()) + + return rows.slice(1).map((line) => { + const values = line.split(",").map((value) => value.trim()) + const record: Record = {} + headers.forEach((header, index) => { + record[header] = values[index] ?? "" + }) + return { + title: record.title || record.Title || "", + slug: record.slug || record.Slug || "", + track: record.track || record.Track || "", + difficulty: record.difficulty || record.Difficulty || "", + description: record.description || record.Description || "", + coverImage: record.coverImage || record.CoverImage || null, + published: + (record.published || record.Published || "").toLowerCase() === "true", + } + }) +} + +const isCourseRowValid = (row: CourseImportRow) => { + return ( + row.title.trim().length > 0 && + row.slug.trim().length > 0 && + row.track.trim().length > 0 && + row.difficulty.trim().length > 0 +======= {section} ))} @@ -313,10 +495,208 @@ const Admin: React.FC = () => { {activeSection === "contracts" && }

+>>>>>>> main ) } const CourseManagement: React.FC = () => { +<<<<<<< HEAD + const { t } = useTranslation() + const [courses, setCourses] = useState(initialCourses) + const [fileName, setFileName] = useState("") + const [previewRows, setPreviewRows] = useState([]) + const [previewErrors, setPreviewErrors] = useState([]) + const [importResults, setImportResults] = useState([]) + const [isSubmitting, setIsSubmitting] = useState(false) + const [alertMessage, setAlertMessage] = useState(null) + + const handleFileUpload = async (event: React.ChangeEvent) => { + setImportResults([]) + setAlertMessage(null) + const file = event.target.files?.[0] + if (!file) { + return + } + + setFileName(file.name) + const contents = await file.text() + let rows: CourseImportRow[] = [] + if (file.name.toLowerCase().endsWith(".json")) { + try { + const parsed = JSON.parse(contents) + rows = Array.isArray(parsed) ? parsed : parsed.courses ?? [] + } catch { + setPreviewErrors([t("admin.import.invalidJson")]) + return + } + } else { + rows = parseCsvText(contents) + } + + const errors: string[] = [] + const normalizedRows = rows.map((row, index) => { + const normalized = { + ...row, + title: row.title?.trim() ?? "", + slug: row.slug?.trim() ?? "", + track: row.track?.trim() ?? "", + difficulty: row.difficulty?.trim() ?? "", + description: row.description?.trim(), + coverImage: row.coverImage?.trim() || null, + published: Boolean(row.published), + } + + if (!isCourseRowValid(normalized)) { + errors.push(`${t("admin.import.invalidRow")} ${index + 1}`) + } + + return normalized + }) + + setPreviewRows(normalizedRows) + setPreviewErrors(errors) + } + + const handleImport = async () => { + setIsSubmitting(true) + setImportResults([]) + setAlertMessage(null) + const token = localStorage.getItem("admin_token") ?? "" + + try { + const response = await fetch("/api/admin/courses/bulk-import", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ courses: previewRows }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.error || t("admin.import.importFailed")) + } + + const data = (await response.json()) as { + results: BulkImportResult[] + total: number + imported: number + } + setImportResults(data.results) + setAlertMessage(t("admin.import.importSuccess", { count: data.imported })) + } catch (error) { + setAlertMessage(String(error)) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+
+

+ {t("admin.import.title")} +

+

+ {t("admin.import.description")} +

+
+
+ +
+ +
+ + {fileName || t("admin.import.noFileSelected")} +
+ {previewErrors.length > 0 && ( +
+ {previewErrors.map((error) => ( +

{error}

+ ))} +
+ )} + {previewRows.length > 0 && ( +
+
+

+ {t("admin.import.previewHeader", { count: previewRows.length })} +

+
+ + + + + + + + + + + + {previewRows.map((row, index) => ( + + + + + + + + ))} + +
{t("admin.import.table.title")}{t("admin.import.table.slug")}{t("admin.import.table.track")}{t("admin.import.table.difficulty")}{t("admin.import.table.published")}
{row.title}{row.slug}{row.track}{row.difficulty}{row.published ? t("admin.import.yes") : t("admin.import.no")}
+
+
+
+ + {t("admin.import.confirmPreview")} +
+
+ )} + {alertMessage && ( +
+ {alertMessage} +
+ )} + {importResults.length > 0 && ( +
+

+ {t("admin.import.resultsHeader")} +

+
    + {importResults.map((result) => ( +
  • + {t("admin.import.rowLabel", { row: result.row })}: {result.success ? t("admin.import.rowSuccess") : t("admin.import.rowFailure")} + {result.errors.length > 0 && ( +
      + {result.errors.map((error) => ( +
    • {error}
    • + ))} +
    + )} +
  • + ))} +
+
+ )} +======= const { data: courses = [], isLoading, @@ -406,14 +786,18 @@ const CourseManagement: React.FC = () => {
))} +>>>>>>> main ) } const MilestoneQueue: React.FC = () => { +<<<<<<< HEAD +======= const { data: courseOptionsData = [], error: courseOptionsError } = useAdminCoursesList() +>>>>>>> main const { milestones, total, @@ -424,6 +808,239 @@ const MilestoneQueue: React.FC = () => { fetchMilestones, approveMilestone, rejectMilestone, +<<<<<<< HEAD + } = useAdminMilestones() + + const [courseFilter, setCourseFilter] = useState("All") + const [statusFilter, setStatusFilter] = useState("pending") + const [dialog, setDialog] = useState<{ + action: "approve" | "reject" + milestone: MilestoneSubmission + } | null>(null) + + useEffect(() => { + void fetchMilestones(1, { + course: courseFilter !== "All" ? courseFilter : undefined, + status: statusFilter, + }) + }, [courseFilter, statusFilter, fetchMilestones]) + + const handlePageChange = (newPage: number) => { + void fetchMilestones(newPage, { + course: courseFilter !== "All" ? courseFilter : undefined, + status: statusFilter, + }) + } + + const handleConfirm = async () => { + if (!dialog) return + const { action, milestone } = dialog + setDialog(null) + if (action === "approve") await approveMilestone(milestone.id) + else await rejectMilestone(milestone.id) + } + + const totalPages = Math.ceil(total / pageSize) + + return ( +
+ {/* Stats bar */} + + + {/* Filters */} +
+
+ + +
+
+ + +
+
+ + {/* Error */} + {error && ( +

+ Error loading milestones: {error} +

+ )} + + {/* Table */} +
+ + + + + + + + + + + + + {loading && ( + + + + )} + + {!loading && milestones.length === 0 && ( + + + + )} + + {!loading && + milestones.map((m) => { + const statusStyles: Record< + MilestoneSubmission["status"], + string + > = { + pending: + "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", + approved: + "text-emerald-400 bg-emerald-400/10 border-emerald-400/30", + rejected: "text-red-400 bg-red-400/10 border-red-400/30", + } + return ( + + + + + + + + + ) + })} + +
LearnerCourseSubmittedEvidenceStatusActions
+ Loading milestones… +
+

+ No milestone submissions found. +

+

+ Try adjusting your filters or check back later. +

+
+ + {m.learnerAddress.slice(0, 8)}… + {m.learnerAddress.slice(-4)} + + + {m.course} + + {new Date(m.submittedAt).toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + })} + + + + + {m.status} + + + {m.status === "pending" && ( +
+ + +
+ )} +
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + Page {page} of {totalPages} ({total} total) + +
+ + +
+
+ )} + + {/* Confirmation dialog */} +======= batchApproveMilestones, batchRejectMilestones, } = useAdminMilestones() @@ -763,11 +1380,16 @@ const MilestoneQueue: React.FC = () => { )} +>>>>>>> main {dialog && ( void handleConfirm()} +>>>>>>> main onCancel={() => setDialog(null)} /> )} @@ -777,6 +1399,29 @@ const MilestoneQueue: React.FC = () => { const UserLookup: React.FC = () => { const [search, setSearch] = useState("") +<<<<<<< HEAD + const [userData, setUserData] = useState(null) + return ( +
+ setSearch(event.target.value)} + /> + + {userData ?

{userData.address}

: null} +======= const [submittedAddress, setSubmittedAddress] = useState(null) const [validationError, setValidationError] = useState(null) const { @@ -910,11 +1555,23 @@ const UserLookup: React.FC = () => { )} +>>>>>>> main
) } const TreasuryControls: React.FC = () => { +<<<<<<< HEAD + const [isPaused, setIsPaused] = useState(false) + return ( +
+ +
+ ) +} +======= const { address } = useWallet() const { data, isLoading, error, refetch } = useAdminContracts() const { @@ -1129,8 +1786,22 @@ const ContractStateCard: React.FC<{ ) } +>>>>>>> main const ContractInfo: React.FC = () => { +<<<<<<< HEAD + return ( +
+ {contractRecords.map((contract) => ( +
+ {contract.name} {contract.updated} +
+ ))} +
+ ) +} + +======= const { data, isLoading, error } = useAdminContracts() const errorMessage = error instanceof Error ? error.message : null @@ -1428,4 +2099,5 @@ const WikiManagement: React.FC = () => { ) } +>>>>>>> main export default Admin diff --git a/src/pages/Courses.tsx b/src/pages/Courses.tsx index 532547a5..0cfe2a5b 100644 --- a/src/pages/Courses.tsx +++ b/src/pages/Courses.tsx @@ -5,10 +5,14 @@ import BookmarkButton from "../components/BookmarkButton" import { CourseFilter } from "../components/CourseFilter" import Pagination from "../components/Pagination" import { CourseCardSkeleton } from "../components/skeletons/CourseCardSkeleton" +<<<<<<< HEAD +import { courses } from "../data/courses" +======= import { EmptyState } from "../components/states/emptyState" import { ErrorState } from "../components/states/errorState" import { useCourses } from "../hooks/useCourses" import { type CourseSummary } from "../types/courses" +>>>>>>> main const levelStyles: Record = { Beginner: "bg-brand-emerald/20 text-brand-emerald border-brand-emerald/20", diff --git a/src/pages/DaoProposals.tsx b/src/pages/DaoProposals.tsx index 49acebcb..11559dd7 100644 --- a/src/pages/DaoProposals.tsx +++ b/src/pages/DaoProposals.tsx @@ -234,10 +234,20 @@ const DaoProposals: React.FC = () => { totalVotes > 0n ? Number((selectedProposal!.votesAgainst * 100n) / totalVotes) : 0 +<<<<<<< HEAD + + const userHasVoted = selectedProposal ? hasVoted(selectedProposal.id) : false + const voteChoice = selectedProposal + ? getVoteChoice(selectedProposal.id) + : null + const governanceTokens = votingPower + const isTokenHolder = governanceTokens > 0n +======= const userHasVoted = selectedProposal?.userVoteSupport === true || selectedProposal?.userVoteSupport === false const voteChoice = selectedProposal?.userVoteSupport ?? null +>>>>>>> main const isWalletConnected = Boolean(walletAddress) const isTokenHolder = votingPower > 0n const voteDisabled = @@ -494,7 +504,16 @@ const DaoProposals: React.FC = () => { {userHasVoted ? (
+<<<<<<< HEAD + You voted{" "} + {voteChoice === null + ? "For/Against" + : voteChoice + ? "For" + : "Against"} +======= You voted {voteChoice ? "Yes" : "No"} +>>>>>>> main
) : (
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 8eaf8b9c..dd8b5f19 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -87,12 +87,52 @@ const Home: React.FC = () => {
+<<<<<<< HEAD + {showOnboarding ? ( + }> + + + ) : ( +
+
+
+

+ New Learner Flow +

+

+ Launch the guided wallet-and-enrollment setup only when you + need it. +

+

+ The onboarding assistant is still available, but it now loads + on demand so the dashboard shell reaches first paint faster on + mobile data. +

+
+ +
+
+ )} + +
+
+
+ LV +
+=======
{/* ── HERO ─────────────────────────────────────────────────────── */}
{/* Logo mark */}
LV +>>>>>>> main
{/* Headline */} @@ -142,6 +182,22 @@ const Home: React.FC = () => {
+<<<<<<< HEAD +
+
+
+
+
+

+ + {t("home.courseProgress.title")} +

+

+ {t("home.courseProgress.desc")} +

+
+
+======= {/* ── HOW IT WORKS ─────────────────────────────────────────────── */}
@@ -196,6 +252,7 @@ const Home: React.FC = () => { style={{ width: `${course.progressPercent}%` }} />
+>>>>>>> main } > @@ -233,6 +290,16 @@ const Home: React.FC = () => {

{description}

+<<<<<<< HEAD + } + > + }> + + + +======= +>>>>>>> main
))}
diff --git a/src/pages/Leaderboard.tsx b/src/pages/Leaderboard.tsx index 50033dd0..ee66f0a6 100644 --- a/src/pages/Leaderboard.tsx +++ b/src/pages/Leaderboard.tsx @@ -12,12 +12,50 @@ const Leaderboard: React.FC = () => { const { t } = useTranslation() const { address: currentUserAddress } = useWallet() +<<<<<<< HEAD + useEffect(() => { + const fetchLeaderboard = async () => { + try { + const response = await fetch( + "http://localhost:4000/api/scholars/leaderboard?page=1&limit=25", + ) + if (!response.ok) throw new Error("Failed to fetch leaderboard") + const result = (await response.json()) as { + rankings?: LeaderboardApiEntry[] + your_rank?: number | null + } + const rankings = Array.isArray(result.rankings) ? result.rankings : [] + const mapped = rankings.map((item, index) => ({ + id: `leader-${item.address}-${item.rank}-${index}`, + address: item.address, + lrnBalance: Number(item.lrn_balance ?? 0), + coursesCompleted: item.courses_completed ?? 0, + joinedDate: new Date(), + lastActive: new Date(), + rank: item.rank, + balance: item.lrn_balance ?? "0", + completedCourses: item.courses_completed ?? 0, + fullAddress: item.address, + })) + setLeaders(mapped) + setMyRank( + typeof result.your_rank === "number" ? result.your_rank : null, + ) + } catch (err) { + console.error(err) + setError("Unable to load rankings. Please try again later.") + } finally { + setIsLoading(false) + } + } +======= const { data: result, isLoading, error, refetch, } = useLeaderboard(currentUserAddress) +>>>>>>> main const leaders = useMemo(() => { const rankings = Array.isArray(result?.rankings) ? result.rankings : [] diff --git a/src/types/contracts.ts b/src/types/contracts.ts index 86419d98..a1662a5d 100644 --- a/src/types/contracts.ts +++ b/src/types/contracts.ts @@ -9,6 +9,8 @@ // --------------------------------------------------------------------------- // Canonical on-chain / shared contract types (as requested) // --------------------------------------------------------------------------- +<<<<<<< HEAD +======= export interface MilestoneReport { id: string @@ -45,6 +47,7 @@ export interface LearnTokenInfo { reputation_score: bigint total_supply: bigint } +>>>>>>> main export type { Proposal, RawContractProposal } from "./governance" // --------------------------------------------------------------------------- diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo new file mode 100644 index 00000000..302ae58a --- /dev/null +++ b/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/App.tsx","./src/i18n.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/ActivityFeed.tsx","./src/components/AddressDisplay.tsx","./src/components/ComingSoon.tsx","./src/components/CommentCard.tsx","./src/components/CommentSection.tsx","./src/components/ConnectAccount.tsx","./src/components/ConnectWalletGuard.tsx","./src/components/CourseCard.tsx","./src/components/CourseFilter.tsx","./src/components/CourseProgressBar.tsx","./src/components/DeferredSection.tsx","./src/components/ErrorBoundary.tsx","./src/components/Footer.tsx","./src/components/FundAccountButton.tsx","./src/components/GetTestUSDCButton.tsx","./src/components/GuessTheNumber.tsx","./src/components/IpfsUpload.tsx","./src/components/LRNBalanceWidget.tsx","./src/components/LanguageSelector.tsx","./src/components/LessonContent.tsx","./src/components/LessonSidebar.tsx","./src/components/MilestoneCelebration.tsx","./src/components/MilestoneReportForm.tsx","./src/components/MilestoneSubmitPanel.tsx","./src/components/MilestoneTracker.tsx","./src/components/NavBar.tsx","./src/components/NetworkBanner.tsx","./src/components/NetworkPill.tsx","./src/components/NetworkPreconnect.tsx","./src/components/OnboardingWizard.tsx","./src/components/Pagination.tsx","./src/components/ProposalCard.tsx","./src/components/ProposalCountdown.test.ts","./src/components/ProposalCountdown.tsx","./src/components/QuizEngine.tsx","./src/components/ReputationBadge.tsx","./src/components/SkeletonLoader.tsx","./src/components/ThemeToggle.tsx","./src/components/TreasuryStatsBar.tsx","./src/components/TxHashLink.tsx","./src/components/WalletAddressPill.tsx","./src/components/WalletButton.tsx","./src/components/WalletToastWatcher.tsx","./src/components/Toast/ToastProvider.tsx","./src/components/debug/ContractExplorerPanel.tsx","./src/components/donor/ActiveVotes.tsx","./src/components/donor/DepositMore.tsx","./src/components/donor/EmptyState.tsx","./src/components/donor/GovernancePower.tsx","./src/components/donor/MyContributions.tsx","./src/components/donor/ScholarsFunded.tsx","./src/components/skeletons/CourseCardSkeleton.tsx","./src/components/skeletons/LessonListSkeleton.tsx","./src/components/treasury/TreasuryHealthChart.tsx","./src/contracts/governance_token.ts","./src/contracts/guess_the_number.ts","./src/contracts/scholarship_treasury.ts","./src/contracts/util.ts","./src/data/courses.ts","./src/data/lessons.ts","./src/hooks/useActivityFeed.ts","./src/hooks/useContractIds.ts","./src/hooks/useCourse.ts","./src/hooks/useDonor.test.tsx","./src/hooks/useDonor.ts","./src/hooks/useGovernance.test.tsx","./src/hooks/useGovernance.ts","./src/hooks/useLearnToken.test.tsx","./src/hooks/useLearnToken.ts","./src/hooks/useNotification.ts","./src/hooks/useScholarshipApplication.ts","./src/hooks/useSubscription.ts","./src/hooks/useWallet.test.tsx","./src/hooks/useWallet.ts","./src/lib/ipfs.ts","./src/pages/Admin.tsx","./src/pages/Courses.tsx","./src/pages/Credential.tsx","./src/pages/Dao.tsx","./src/pages/DaoProposals.tsx","./src/pages/DaoPropose.tsx","./src/pages/Dashboard.tsx","./src/pages/Debug.tsx","./src/pages/Donor.tsx","./src/pages/Home.tsx","./src/pages/Leaderboard.tsx","./src/pages/Learn.tsx","./src/pages/LessonView.tsx","./src/pages/NotFound.tsx","./src/pages/Profile.tsx","./src/pages/ScholarMilestones.tsx","./src/pages/ScholarshipApply.tsx","./src/pages/Treasury.tsx","./src/providers/NotificationProvider.tsx","./src/providers/WalletProvider.tsx","./src/test/setup.ts","./src/test/mocks/contracts.ts","./src/test/mocks/wallet.ts","./src/tests/setup.ts","./src/types/errors.ts","./src/types/governance.ts","./src/types/milestone.ts","./src/util/contract.test.ts","./src/util/contract.ts","./src/util/error.ts","./src/util/friendbot.test.ts","./src/util/friendbot.ts","./src/util/mockLeaderboardData.ts","./src/util/profileData.test.ts","./src/util/profileData.ts","./src/util/reputationRank.test.ts","./src/util/reputationRank.ts","./src/util/scholarshipApplications.ts","./src/util/scholarshipTreasury.ts","./src/util/storage.test.ts","./src/util/storage.ts","./src/util/theme.test.ts","./src/util/theme.ts","./src/util/tokenFormat.test.ts","./src/util/tokenFormat.ts","./src/util/usdc.ts","./src/util/wallet.test.ts","./src/util/wallet.ts","./src/utils/errors.ts","./e2e/critical-flows.spec.ts","./e2e/fixtures/mock-horizon.ts","./e2e/fixtures/mock-wallet.ts","./playwright.config.ts","./reset.d.ts"],"errors":true,"version":"5.9.3"} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index b2a2a005..94bf6562 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,7 +9,10 @@ export default defineConfig({ setupFiles: ["./src/test/setup.ts"], include: ["src/**/*.test.{ts,tsx}"], env: { +<<<<<<< HEAD +======= NODE_ENV: "development", +>>>>>>> main PUBLIC_SCHOLARSHIP_TREASURY_CONTRACT: "CSCHOL1234567890ABCDEFGHIJKLMN9876543210ZYXWVUTSRQPO", PUBLIC_GOVERNANCE_TOKEN_CONTRACT: