From a040df360456190db3047bd6b3a32749d7c3d5be Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Thu, 27 Aug 2026 01:05:35 -0300 Subject: [PATCH] feat(stablecoin): expose redemption price state --- modules/stablecoin/README.md | 15 ++- .../stablecoin/ffi/include/stablecoin_ffi.h | 8 ++ modules/stablecoin/ffi/src/api/decode.rs | 41 ++++++- modules/stablecoin/ffi/src/api/mod.rs | 8 +- modules/stablecoin/ffi/src/api/request.rs | 7 ++ modules/stablecoin/ffi/src/api/tests.rs | 112 +++++++++++++++++- modules/stablecoin/ffi/src/ffi.rs | 19 ++- modules/stablecoin/ffi/src/lib.rs | 5 +- modules/stablecoin/ffi/tests/public_api.rs | 7 +- .../stablecoin/src/stablecoin_module_impl.cpp | 27 +++++ .../stablecoin/src/stablecoin_module_impl.h | 4 + .../src/stablecoin_module_support.cpp | 2 + .../tests/mocks/mock_stablecoin_ffi.cpp | 4 + .../tests/stablecoin_module_impl_test.cpp | 104 +++++++++++++++- .../tests/stablecoin_module_support_test.cpp | 8 ++ 15 files changed, 353 insertions(+), 18 deletions(-) diff --git a/modules/stablecoin/README.md b/modules/stablecoin/README.md index e51fcb41..5e267b94 100644 --- a/modules/stablecoin/README.md +++ b/modules/stablecoin/README.md @@ -1,9 +1,9 @@ # Stablecoin core module `stablecoin_module` is a headless Logos `core` module for the LEZ Stablecoin -Program. It exposes deployment discovery, protocol-parameter reads, and -protocol initialization through the same universal API used by `logoscore` and -UI modules. +Program. It exposes deployment discovery, protocol-state reads, and protocol +initialization through the same universal API used by `logoscore` and UI +modules. The Qt-free C++ adapter handles live wallet reads and transaction submission. `stablecoin_ffi` owns exact account decoding, PDA derivation, request @@ -44,6 +44,15 @@ includes the account ID in base58 and lowercase hexadecimal form plus `accumulatedRateAtLastAccrual` and `lastAccruedAt` as decimal strings. It does not project the accumulator to the current time. +### `redemptionPriceState()` + +Reads the singleton Redemption Price State account through `lez_core`, verifies +its PDA and owner, and exactly decodes its stored controller state. The result +includes the account ID in base58 and lowercase hexadecimal form plus +`redemptionPriceAtLastUpdate`, `redemptionRatePerMillisecond`, +`controllerIntegralTerm`, and `lastUpdatedAt` as decimal strings. It does not +project the current redemption price or simulate a controller update. + ### `initializeProgram(request)` Required request fields: diff --git a/modules/stablecoin/ffi/include/stablecoin_ffi.h b/modules/stablecoin/ffi/include/stablecoin_ffi.h index 5a023ac4..519e5fab 100644 --- a/modules/stablecoin/ffi/include/stablecoin_ffi.h +++ b/modules/stablecoin/ffi/include/stablecoin_ffi.h @@ -38,6 +38,14 @@ char *stablecoin_decode_protocol_parameters(const char *request_json); */ char *stablecoin_decode_stability_fee_accumulator(const char *request_json); +/** + * Decodes and validates the singleton `RedemptionPriceState` account. + * + * # Safety + * `request_json` must be null or point to a live NUL-terminated byte string. + */ +char *stablecoin_decode_redemption_price_state(const char *request_json); + /** * Builds the exact wallet submission plan for `InitializeProgram`. * diff --git a/modules/stablecoin/ffi/src/api/decode.rs b/modules/stablecoin/ffi/src/api/decode.rs index 17ab3020..c88accf6 100644 --- a/modules/stablecoin/ffi/src/api/decode.rs +++ b/modules/stablecoin/ffi/src/api/decode.rs @@ -1,12 +1,14 @@ use serde_json::{json, Value}; use stablecoin_core::{ - compute_protocol_parameters_pda, compute_stability_fee_accumulator_pda, ProtocolParameters, + compute_protocol_parameters_pda, compute_redemption_price_state_pda, + compute_stability_fee_accumulator_pda, ProtocolParameters, RedemptionPriceState, StabilityFeeAccumulator, }; use super::{ parse_stablecoin_program_id, DecodeProtocolParametersRequest, - DecodeStabilityFeeAccumulatorRequest, StablecoinApiError, StablecoinResult, + DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest, StablecoinApiError, + StablecoinResult, }; use crate::account::{account_id_hex, decode_account}; @@ -48,6 +50,27 @@ pub fn decode_stability_fee_accumulator( Ok(stability_fee_accumulator_value(account_id, &accumulator)) } +pub fn decode_redemption_price_state( + request: DecodeRedemptionPriceStateRequest, +) -> StablecoinResult { + let stablecoin_program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?; + let (account_id, account) = decode_account(&request.redemption_price_state) + .map_err(|_| StablecoinApiError::new("account_read_failed"))?; + + if account_id != compute_redemption_price_state_pda(stablecoin_program_id) { + return Err(StablecoinApiError::new( + "redemption_price_state_pda_mismatch", + )); + } + if account.program_owner != stablecoin_program_id { + return Err(StablecoinApiError::new("stablecoin_program_mismatch")); + } + + let state = RedemptionPriceState::try_from(&account.data) + .map_err(|_| StablecoinApiError::new("invalid_redemption_price_state_data"))?; + Ok(redemption_price_state_value(account_id, &state)) +} + fn parameters_value( account_id: lee_core::account::AccountId, parameters: &ProtocolParameters, @@ -89,3 +112,17 @@ fn stability_fee_accumulator_value( "lastAccruedAt": accumulator.last_accrued_at.to_string(), }) } + +fn redemption_price_state_value( + account_id: lee_core::account::AccountId, + state: &RedemptionPriceState, +) -> Value { + json!({ + "accountId": account_id.to_string(), + "accountIdHex": account_id_hex(account_id), + "redemptionPriceAtLastUpdate": state.redemption_price_at_last_update.to_string(), + "redemptionRatePerMillisecond": state.redemption_rate_per_millisecond.to_string(), + "controllerIntegralTerm": state.controller_integral_term.to_string(), + "lastUpdatedAt": state.last_updated_at.to_string(), + }) +} diff --git a/modules/stablecoin/ffi/src/api/mod.rs b/modules/stablecoin/ffi/src/api/mod.rs index 9971f8f4..8d8d2dbd 100644 --- a/modules/stablecoin/ffi/src/api/mod.rs +++ b/modules/stablecoin/ffi/src/api/mod.rs @@ -10,12 +10,14 @@ mod tests; use std::{error::Error, fmt}; -pub use decode::{decode_protocol_parameters, decode_stability_fee_accumulator}; +pub use decode::{ + decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator, +}; pub use plan::initialize_program_plan; pub use program::program_info; pub use request::{ - DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest, - InitializeProgramPlanRequest, ProgramInfoRequest, + DecodeProtocolParametersRequest, DecodeRedemptionPriceStateRequest, + DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest, }; use serde_json::Value; diff --git a/modules/stablecoin/ffi/src/api/request.rs b/modules/stablecoin/ffi/src/api/request.rs index 277f82a5..735fc006 100644 --- a/modules/stablecoin/ffi/src/api/request.rs +++ b/modules/stablecoin/ffi/src/api/request.rs @@ -26,6 +26,13 @@ pub struct DecodeStabilityFeeAccumulatorRequest { pub stability_fee_accumulator: AccountRead, } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DecodeRedemptionPriceStateRequest { + pub stablecoin_program_id: String, + pub redemption_price_state: AccountRead, +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct InitializeProgramPlanRequest { diff --git a/modules/stablecoin/ffi/src/api/tests.rs b/modules/stablecoin/ffi/src/api/tests.rs index 00a3c397..aedf497e 100644 --- a/modules/stablecoin/ffi/src/api/tests.rs +++ b/modules/stablecoin/ffi/src/api/tests.rs @@ -8,15 +8,16 @@ use serde_json::{json, Value}; use stablecoin_core::{ compute_protocol_parameters_pda, compute_redemption_price_state_pda, compute_stability_fee_accumulator_pda, compute_stablecoin_definition_pda, - compute_stablecoin_master_holding_pda, Instruction, ProtocolParameters, + compute_stablecoin_master_holding_pda, Instruction, ProtocolParameters, RedemptionPriceState, StabilityFeeAccumulator, }; use token_core::TokenDefinition; use twap_oracle_core::OraclePriceAccount; use super::{ - decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan, - program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest, + decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator, + initialize_program_plan, program_info, DecodeProtocolParametersRequest, + DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult, }; use crate::account::{account_id_hex, account_read, program_id_bytes}; @@ -112,6 +113,28 @@ fn accumulator_request( } } +fn redemption_price_state(controller_integral_term: i128) -> RedemptionPriceState { + RedemptionPriceState { + redemption_price_at_last_update: u128::MAX, + redemption_rate_per_millisecond: u128::MAX, + controller_integral_term, + last_updated_at: u64::MAX, + } +} + +fn redemption_price_state_request( + state: &RedemptionPriceState, +) -> DecodeRedemptionPriceStateRequest { + let account_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID); + DecodeRedemptionPriceStateRequest { + stablecoin_program_id: program_id_hex(), + redemption_price_state: account_read( + account_id, + &account(STABLECOIN_PROGRAM_ID, Data::from(state)), + ), + } +} + fn initialize_request() -> InitializeProgramPlanRequest { let collateral_id = id(10); let stablecoin_definition_id = compute_stablecoin_definition_pda(STABLECOIN_PROGRAM_ID); @@ -386,6 +409,89 @@ fn stability_fee_accumulator_decode_rejects_truncated_and_trailing_data() { } } +#[test] +fn redemption_price_state_decode_preserves_fixed_id_and_boundary_values() { + let expected_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID); + assert_eq!( + account_id_hex(expected_id), + "8ec72eaff1c70ac76ed0c139026671fc9991cae1466749dffb3280a5a5aed533" + ); + assert_eq!( + expected_id.to_string(), + "AcM3xWAMKUEPPzCjT1EHssertgGhvDhLe8KGP1uvbRjp" + ); + + for controller_integral_term in [1, 0, -1, i128::MIN, i128::MAX] { + let state = redemption_price_state(controller_integral_term); + let value = ok(decode_redemption_price_state( + redemption_price_state_request(&state), + )); + + assert_eq!(value["accountId"], expected_id.to_string()); + assert_eq!(value["accountIdHex"], account_id_hex(expected_id)); + assert_eq!(value["redemptionPriceAtLastUpdate"], u128::MAX.to_string()); + assert_eq!(value["redemptionRatePerMillisecond"], u128::MAX.to_string()); + assert_eq!( + value["controllerIntegralTerm"], + controller_integral_term.to_string() + ); + assert_eq!(value["lastUpdatedAt"], u64::MAX.to_string()); + } +} + +#[test] +fn redemption_price_state_decode_rejects_failed_reads_and_wrong_identity() { + let state = redemption_price_state(0); + + for status in ["not_found", "backend_error"] { + let mut failed = redemption_price_state_request(&state); + failed.redemption_price_state.status = String::from(status); + failed.redemption_price_state.account = None; + assert_error(decode_redemption_price_state(failed), "account_read_failed"); + } + + let mut wrong_pda = redemption_price_state_request(&state); + wrong_pda.redemption_price_state.id = account_id_hex(id(20)); + assert_error( + decode_redemption_price_state(wrong_pda), + "redemption_price_state_pda_mismatch", + ); + + let mut wrong_owner = redemption_price_state_request(&state); + if let Some(account) = &mut wrong_owner.redemption_price_state.account { + account.program_owner = hex::encode(program_id_bytes(TOKEN_PROGRAM_ID)); + } + assert_error( + decode_redemption_price_state(wrong_owner), + "stablecoin_program_mismatch", + ); +} + +#[test] +fn redemption_price_state_decode_rejects_truncated_and_trailing_data() { + let state = redemption_price_state(i128::MIN); + let account_id = compute_redemption_price_state_pda(STABLECOIN_PROGRAM_ID); + let encoded = Data::from(&state).as_ref().to_vec(); + + for malformed in [encoded[..encoded.len() - 1].to_vec(), { + let mut trailing = encoded.clone(); + trailing.push(0); + trailing + }] { + let request = DecodeRedemptionPriceStateRequest { + stablecoin_program_id: program_id_hex(), + redemption_price_state: account_read( + account_id, + &account(STABLECOIN_PROGRAM_ID, ok(Data::try_from(malformed))), + ), + }; + assert_error( + decode_redemption_price_state(request), + "invalid_redemption_price_state_data", + ); + } +} + #[test] fn initialize_plan_round_trips_all_boundary_values_and_exact_account_contract() { let request = initialize_request(); diff --git a/modules/stablecoin/ffi/src/ffi.rs b/modules/stablecoin/ffi/src/ffi.rs index d7903d40..48845543 100644 --- a/modules/stablecoin/ffi/src/ffi.rs +++ b/modules/stablecoin/ffi/src/ffi.rs @@ -6,8 +6,9 @@ use std::{ use serde::{de::DeserializeOwned, Serialize}; use crate::api::{ - self, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest, - InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult, + self, DecodeProtocolParametersRequest, DecodeRedemptionPriceStateRequest, + DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest, + StablecoinResult, }; #[derive(Serialize)] @@ -126,6 +127,20 @@ pub unsafe extern "C" fn stablecoin_decode_stability_fee_accumulator( } } +#[unsafe(no_mangle)] +/// Decodes and validates the singleton `RedemptionPriceState` account. +/// +/// # Safety +/// `request_json` must be null or point to a live NUL-terminated byte string. +pub unsafe extern "C" fn stablecoin_decode_redemption_price_state( + request_json: *const c_char, +) -> *mut c_char { + // SAFETY: Forwarded from this function's caller contract. + unsafe { + call::(request_json, api::decode_redemption_price_state) + } +} + #[unsafe(no_mangle)] /// Builds the exact wallet submission plan for `InitializeProgram`. /// diff --git a/modules/stablecoin/ffi/src/lib.rs b/modules/stablecoin/ffi/src/lib.rs index 81af49da..93e62db7 100644 --- a/modules/stablecoin/ffi/src/lib.rs +++ b/modules/stablecoin/ffi/src/lib.rs @@ -7,8 +7,9 @@ pub mod api; pub use account::{AccountRead, WalletAccount}; pub use api::{ - decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan, - program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest, + decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator, + initialize_program_plan, program_info, DecodeProtocolParametersRequest, + DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinApiError, StablecoinResponse, StablecoinResult, }; diff --git a/modules/stablecoin/ffi/tests/public_api.rs b/modules/stablecoin/ffi/tests/public_api.rs index 28e07e29..04eaa8d7 100644 --- a/modules/stablecoin/ffi/tests/public_api.rs +++ b/modules/stablecoin/ffi/tests/public_api.rs @@ -1,6 +1,7 @@ use stablecoin_ffi::{ - decode_protocol_parameters, decode_stability_fee_accumulator, initialize_program_plan, - program_info, DecodeProtocolParametersRequest, DecodeStabilityFeeAccumulatorRequest, + decode_protocol_parameters, decode_redemption_price_state, decode_stability_fee_accumulator, + initialize_program_plan, program_info, DecodeProtocolParametersRequest, + DecodeRedemptionPriceStateRequest, DecodeStabilityFeeAccumulatorRequest, InitializeProgramPlanRequest, ProgramInfoRequest, StablecoinResult, }; @@ -11,5 +12,7 @@ fn crate_root_reexports_stablecoin_surface() { decode_protocol_parameters; let _decode_accumulator: fn(DecodeStabilityFeeAccumulatorRequest) -> StablecoinResult = decode_stability_fee_accumulator; + let _decode_redemption_state: fn(DecodeRedemptionPriceStateRequest) -> StablecoinResult = + decode_redemption_price_state; let _initialize: fn(InitializeProgramPlanRequest) -> StablecoinResult = initialize_program_plan; } diff --git a/modules/stablecoin/src/stablecoin_module_impl.cpp b/modules/stablecoin/src/stablecoin_module_impl.cpp index f39e208a..85c8cdfd 100644 --- a/modules/stablecoin/src/stablecoin_module_impl.cpp +++ b/modules/stablecoin/src/stablecoin_module_impl.cpp @@ -305,6 +305,33 @@ LogosMap StablecoinModuleImpl::stabilityFeeAccumulator() { }); } +LogosMap StablecoinModuleImpl::redemptionPriceState() { + return guarded([&]() -> LogosMap { + std::string error; + const json info = stablecoinProgramInfo(error); + if (!info.is_object()) return publicError(error.empty() ? "backend_error" : error); + + const json read = readPublicAccount(jsonString(info, "redemptionPriceStateIdHex")); + const std::string status = jsonString(read, "status"); + if (status == "not_found") return publicError("not_initialized"); + if (status != "ok") return publicError("account_read_failed"); + + const FfiResult decoded = callStablecoin( + stablecoin_decode_redemption_price_state, + { + {"stablecoinProgramId", info["programIdHex"]}, + {"redemptionPriceState", read}, + }); + if (!decoded.ok) { + return publicError(stablecoin_module::detail::stableFfiError(decoded.error)); + } + + LogosMap result = publicOk(); + result["redemptionPriceState"] = decoded.value; + return result; + }); +} + LogosMap StablecoinModuleImpl::submitPlan(const nlohmann::json& plan) { const auto accounts_field = plan.find("accountIds"); const auto signers_field = plan.find("signingRequirements"); diff --git a/modules/stablecoin/src/stablecoin_module_impl.h b/modules/stablecoin/src/stablecoin_module_impl.h index 172cba59..519728b4 100644 --- a/modules/stablecoin/src/stablecoin_module_impl.h +++ b/modules/stablecoin/src/stablecoin_module_impl.h @@ -28,6 +28,10 @@ class StablecoinModuleImpl : public LogosModuleContext { /// Returns the stored snapshot without projecting it to the current time. LogosMap stabilityFeeAccumulator(); + /// Reads and exactly decodes the singleton RedemptionPriceState account. + /// Returns stored controller state without projecting the current price. + LogosMap redemptionPriceState(); + /// Initializes the stablecoin protocol. Request fields are `adminId`, /// `freezeAuthorityId`, `collateralDefinitionId`, `marketPriceOracleId`, /// `initialStabilityFeePerMillisecond`, diff --git a/modules/stablecoin/src/stablecoin_module_support.cpp b/modules/stablecoin/src/stablecoin_module_support.cpp index d2ad08b4..0c1e9258 100644 --- a/modules/stablecoin/src/stablecoin_module_support.cpp +++ b/modules/stablecoin/src/stablecoin_module_support.cpp @@ -165,11 +165,13 @@ std::string stableFfiError(const std::string& error) { "invalid_program_binary", "invalid_program_id", "invalid_protocol_parameters_data", + "invalid_redemption_price_state_data", "invalid_stability_fee_accumulator_data", "invalid_stablecoin_name", "oracle_asset_mismatch", "program_id_mismatch", "protocol_parameters_pda_mismatch", + "redemption_price_state_pda_mismatch", "stability_fee_accumulator_pda_mismatch", "stablecoin_program_mismatch", }; diff --git a/modules/stablecoin/tests/mocks/mock_stablecoin_ffi.cpp b/modules/stablecoin/tests/mocks/mock_stablecoin_ffi.cpp index 913aa70a..d384d12f 100644 --- a/modules/stablecoin/tests/mocks/mock_stablecoin_ffi.cpp +++ b/modules/stablecoin/tests/mocks/mock_stablecoin_ffi.cpp @@ -35,6 +35,10 @@ extern "C" char* stablecoin_decode_stability_fee_accumulator(const char*) { return copyMockResponse("stablecoin_decode_stability_fee_accumulator"); } +extern "C" char* stablecoin_decode_redemption_price_state(const char*) { + return copyMockResponse("stablecoin_decode_redemption_price_state"); +} + extern "C" char* stablecoin_initialize_program_plan(const char*) { return copyMockResponse("stablecoin_initialize_program_plan"); } diff --git a/modules/stablecoin/tests/stablecoin_module_impl_test.cpp b/modules/stablecoin/tests/stablecoin_module_impl_test.cpp index 0c886823..8322bb2d 100644 --- a/modules/stablecoin/tests/stablecoin_module_impl_test.cpp +++ b/modules/stablecoin/tests/stablecoin_module_impl_test.cpp @@ -18,6 +18,7 @@ using json = nlohmann::json; const std::string PROGRAM_ID_HEX(64, '1'); const std::string ACCUMULATOR_ID_HEX(64, '2'); +const std::string REDEMPTION_STATE_ID_HEX(64, '4'); class ScopedEnvironment { public: @@ -58,7 +59,7 @@ json programInfoValue() { {"stabilityFeeAccumulatorId", "stability-fee-accumulator-id"}, {"stabilityFeeAccumulatorIdHex", ACCUMULATOR_ID_HEX}, {"redemptionPriceStateId", "redemption-price-state-id"}, - {"redemptionPriceStateIdHex", std::string(64, '4')}, + {"redemptionPriceStateIdHex", REDEMPTION_STATE_ID_HEX}, {"stablecoinDefinitionId", "stablecoin-definition-id"}, {"stablecoinDefinitionIdHex", std::string(64, '5')}, {"stablecoinMasterHoldingId", "stablecoin-master-holding-id"}, @@ -194,3 +195,104 @@ LOGOS_TEST(stability_fee_accumulator_preserves_stable_decoder_errors) { LOGOS_ASSERT_EQ( context.cFunctionCallCount("stablecoin_decode_stability_fee_accumulator"), 1); } + +LOGOS_TEST(redemption_price_state_reads_once_and_returns_exact_snapshot) { + ScopedEnvironment program_id("STABLECOIN_PROGRAM_ID", PROGRAM_ID_HEX.c_str()); + ScopedEnvironment program_binary("STABLECOIN_PROGRAM_BIN", nullptr); + LogosTestContext context("stablecoin_module"); + LogosModules modules(context.api()); + StablecoinModuleImpl module; + attachModules(module, modules); + + const json decoded = { + {"accountId", "redemption-price-state-id"}, + {"accountIdHex", REDEMPTION_STATE_ID_HEX}, + {"redemptionPriceAtLastUpdate", "340282366920938463463374607431768211455"}, + {"redemptionRatePerMillisecond", "340282366920938463463374607431768211455"}, + {"controllerIntegralTerm", "-170141183460469231731687303715884105728"}, + {"lastUpdatedAt", "18446744073709551615"}, + }; + const std::string program_info_response = successEnvelope(programInfoValue()); + const std::string decoder_response = successEnvelope(decoded); + context.mockCFunction("stablecoin_program_info").returns(program_info_response); + context.mockCFunction("stablecoin_decode_redemption_price_state") + .returns(decoder_response); + context.mockModule("lez_core", "get_account_public").returns(initializedAccount()); + + const LogosMap response = module.redemptionPriceState(); + + LOGOS_ASSERT_EQ(response["status"].get(), std::string("ok")); + LOGOS_ASSERT_EQ(response["error"].get(), std::string()); + LOGOS_ASSERT_EQ(response["redemptionPriceState"], decoded); + LOGOS_ASSERT_EQ(context.moduleCallCount("lez_core", "get_account_public"), 1); + LOGOS_ASSERT_TRUE(context.moduleCalledWith( + "lez_core", + "get_account_public", + QVariantList{QVariant(QString::fromStdString(REDEMPTION_STATE_ID_HEX))})); + LOGOS_ASSERT_EQ( + context.cFunctionCallCount("stablecoin_decode_redemption_price_state"), 1); +} + +LOGOS_TEST(redemption_price_state_maps_missing_account_to_not_initialized) { + ScopedEnvironment program_id("STABLECOIN_PROGRAM_ID", PROGRAM_ID_HEX.c_str()); + ScopedEnvironment program_binary("STABLECOIN_PROGRAM_BIN", nullptr); + LogosTestContext context("stablecoin_module"); + LogosModules modules(context.api()); + StablecoinModuleImpl module; + attachModules(module, modules); + + const std::string program_info_response = successEnvelope(programInfoValue()); + context.mockCFunction("stablecoin_program_info").returns(program_info_response); + context.mockModule("lez_core", "get_account_public").returns(""); + + const LogosMap response = module.redemptionPriceState(); + + assertError(response, "not_initialized"); + LOGOS_ASSERT_EQ(context.moduleCallCount("lez_core", "get_account_public"), 1); + LOGOS_ASSERT_EQ( + context.cFunctionCallCount("stablecoin_decode_redemption_price_state"), 0); +} + +LOGOS_TEST(redemption_price_state_rejects_malformed_account_response) { + ScopedEnvironment program_id("STABLECOIN_PROGRAM_ID", PROGRAM_ID_HEX.c_str()); + ScopedEnvironment program_binary("STABLECOIN_PROGRAM_BIN", nullptr); + LogosTestContext context("stablecoin_module"); + LogosModules modules(context.api()); + StablecoinModuleImpl module; + attachModules(module, modules); + + const std::string program_info_response = successEnvelope(programInfoValue()); + context.mockCFunction("stablecoin_program_info").returns(program_info_response); + context.mockModule("lez_core", "get_account_public").returns("not-json"); + + const LogosMap response = module.redemptionPriceState(); + + assertError(response, "account_read_failed"); + LOGOS_ASSERT_EQ(context.moduleCallCount("lez_core", "get_account_public"), 1); + LOGOS_ASSERT_EQ( + context.cFunctionCallCount("stablecoin_decode_redemption_price_state"), 0); +} + +LOGOS_TEST(redemption_price_state_preserves_stable_decoder_errors) { + ScopedEnvironment program_id("STABLECOIN_PROGRAM_ID", PROGRAM_ID_HEX.c_str()); + ScopedEnvironment program_binary("STABLECOIN_PROGRAM_BIN", nullptr); + LogosTestContext context("stablecoin_module"); + LogosModules modules(context.api()); + StablecoinModuleImpl module; + attachModules(module, modules); + + const std::string program_info_response = successEnvelope(programInfoValue()); + const std::string decoder_response = failureEnvelope( + "redemption_price_state_pda_mismatch"); + context.mockCFunction("stablecoin_program_info").returns(program_info_response); + context.mockCFunction("stablecoin_decode_redemption_price_state") + .returns(decoder_response); + context.mockModule("lez_core", "get_account_public").returns(initializedAccount()); + + const LogosMap response = module.redemptionPriceState(); + + assertError(response, "redemption_price_state_pda_mismatch"); + LOGOS_ASSERT_EQ(context.moduleCallCount("lez_core", "get_account_public"), 1); + LOGOS_ASSERT_EQ( + context.cFunctionCallCount("stablecoin_decode_redemption_price_state"), 1); +} diff --git a/modules/stablecoin/tests/stablecoin_module_support_test.cpp b/modules/stablecoin/tests/stablecoin_module_support_test.cpp index 3d17d40f..d1cefc4b 100644 --- a/modules/stablecoin/tests/stablecoin_module_support_test.cpp +++ b/modules/stablecoin/tests/stablecoin_module_support_test.cpp @@ -108,6 +108,14 @@ LOGOS_TEST(ffi_error_mapping_preserves_only_public_codes) { stablecoin_module::detail::stableFfiError( "stability_fee_accumulator_pda_mismatch"), std::string("stability_fee_accumulator_pda_mismatch")); + LOGOS_ASSERT_EQ( + stablecoin_module::detail::stableFfiError( + "invalid_redemption_price_state_data"), + std::string("invalid_redemption_price_state_data")); + LOGOS_ASSERT_EQ( + stablecoin_module::detail::stableFfiError( + "redemption_price_state_pda_mismatch"), + std::string("redemption_price_state_pda_mismatch")); LOGOS_ASSERT_EQ( stablecoin_module::detail::stableFfiError("internal parse detail"), std::string("backend_error"));