Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions modules/stablecoin/README.md
Original file line number Diff line number Diff line change
@@ -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-parameter and position
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
Expand Down Expand Up @@ -36,6 +36,30 @@ Reads the singleton Protocol Parameters account through
`lez_core`, verifies its PDA and owner, and exactly decodes its
data. All `u128`, `i128`, and `u64` values are returned as decimal strings.

### `positionAccount(request)`

Required request fields:

| Field | Type |
| --- | --- |
| `ownerId` | base58 or 64-character hexadecimal account ID |
| `positionNonce` | exact `u64` decimal string |

The module derives the position PDA from `(ownerId, positionNonce)`, derives
the position's collateral-vault PDA, and performs one direct public-account
read. It does not enumerate wallet or global accounts.

On success, the response adds a `position` object containing the owner,
position, and vault IDs in base58 and lowercase hexadecimal form. It also
returns `positionNonce`, `collateralAmount`, `normalizedDebtAmount`, and
`openedAt` as exact decimal strings. The account owner, address, stored owner,
stored nonce, and stored vault must all match the derived identity.

When the derived position account does not exist, the method returns
`{ "status": "error", "error": "not_found" }` and still adds `position` with
the derived owner, position, and vault IDs. No account decoder runs for this
ordinary absence case.

### `initializeProgram(request)`

Required request fields:
Expand Down
16 changes: 16 additions & 0 deletions modules/stablecoin/ffi/include/stablecoin_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ char *stablecoin_program_info(const char *request_json);
*/
char *stablecoin_decode_protocol_parameters(const char *request_json);

/**
* Derives the position and collateral-vault account IDs for an owner and nonce.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_position_info(const char *request_json);

/**
* Decodes and validates a stablecoin `Position` account.
*
* # Safety
* `request_json` must be null or point to a live NUL-terminated byte string.
*/
char *stablecoin_decode_position(const char *request_json);

/**
* Builds the exact wallet submission plan for `InitializeProgram`.
*
Expand Down
5 changes: 4 additions & 1 deletion modules/stablecoin/ffi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod decode;
mod plan;
mod position;
mod program;
mod request;

Expand All @@ -12,9 +13,11 @@ use std::{error::Error, fmt};

pub use decode::decode_protocol_parameters;
pub use plan::initialize_program_plan;
pub use position::{decode_position, position_info};
pub use program::program_info;
pub use request::{
DecodeProtocolParametersRequest, InitializeProgramPlanRequest, ProgramInfoRequest,
DecodePositionRequest, DecodeProtocolParametersRequest, InitializeProgramPlanRequest,
PositionInfoRequest, ProgramInfoRequest,
};
use serde_json::Value;

Expand Down
100 changes: 100 additions & 0 deletions modules/stablecoin/ffi/src/api/position.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use lee_core::account::AccountId;
use serde_json::{json, Value};
use stablecoin_core::{compute_position_pda, compute_position_vault_pda, Position};

use super::{
parse_stablecoin_program_id, DecodePositionRequest, PositionInfoRequest, StablecoinApiError,
StablecoinResult,
};
use crate::account::{account_id_from_hex, account_id_hex, decode_account};

pub fn position_info(request: PositionInfoRequest) -> StablecoinResult {
let stablecoin_program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let owner_id = parse_owner_id(&request.owner_id)?;
let position_nonce = parse_position_nonce(&request.position_nonce)?;
let position_id = compute_position_pda(stablecoin_program_id, owner_id, position_nonce);
let vault_id = compute_position_vault_pda(stablecoin_program_id, position_id);

Ok(position_identity_value(
owner_id,
position_nonce,
position_id,
vault_id,
))
}

pub fn decode_position(request: DecodePositionRequest) -> StablecoinResult {
let stablecoin_program_id = parse_stablecoin_program_id(&request.stablecoin_program_id)?;
let owner_id = parse_owner_id(&request.owner_id)?;
let position_nonce = parse_position_nonce(&request.position_nonce)?;
let expected_position_id =
compute_position_pda(stablecoin_program_id, owner_id, position_nonce);
let expected_vault_id = compute_position_vault_pda(stablecoin_program_id, expected_position_id);
let (position_id, account) = decode_account(&request.position)
.map_err(|_| StablecoinApiError::new("account_read_failed"))?;

if position_id != expected_position_id {
return Err(StablecoinApiError::new("position_pda_mismatch"));
}
if account.program_owner != stablecoin_program_id {
return Err(StablecoinApiError::new("stablecoin_program_mismatch"));
}

let position = Position::try_from(&account.data)
.map_err(|_| StablecoinApiError::new("invalid_position_data"))?;
if position.owner_account_id != owner_id {
return Err(StablecoinApiError::new("position_owner_mismatch"));
}
if position.position_nonce != position_nonce {
return Err(StablecoinApiError::new("position_nonce_mismatch"));
}
if position.vault_account_id != expected_vault_id {
return Err(StablecoinApiError::new("position_vault_mismatch"));
}

let mut value = position_identity_value(
owner_id,
position_nonce,
expected_position_id,
expected_vault_id,
);
value["collateralAmount"] = json!(position.collateral_amount.to_string());
value["normalizedDebtAmount"] = json!(position.normalized_debt_amount.to_string());
value["openedAt"] = json!(position.opened_at.to_string());
Ok(value)
}

fn parse_owner_id(value: &str) -> Result<AccountId, StablecoinApiError> {
let owner_id = account_id_from_hex(value, "owner id")
.map_err(|_| StablecoinApiError::new("invalid_account_id"))?;
if owner_id.value() == &[0_u8; 32] {
return Err(StablecoinApiError::new("invalid_account_id"));
}
Ok(owner_id)
}

fn parse_position_nonce(value: &str) -> Result<u64, StablecoinApiError> {
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(StablecoinApiError::new("invalid_numeric_value"));
}
value
.parse::<u64>()
.map_err(|_| StablecoinApiError::new("invalid_numeric_value"))
}

fn position_identity_value(
owner_id: AccountId,
position_nonce: u64,
position_id: AccountId,
vault_id: AccountId,
) -> Value {
json!({
"ownerId": owner_id.to_string(),
"ownerIdHex": account_id_hex(owner_id),
"positionNonce": position_nonce.to_string(),
"positionId": position_id.to_string(),
"positionIdHex": account_id_hex(position_id),
"vaultId": vault_id.to_string(),
"vaultIdHex": account_id_hex(vault_id),
})
}
17 changes: 17 additions & 0 deletions modules/stablecoin/ffi/src/api/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ pub struct DecodeProtocolParametersRequest {
pub protocol_parameters: AccountRead,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PositionInfoRequest {
pub stablecoin_program_id: String,
pub owner_id: String,
pub position_nonce: String,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DecodePositionRequest {
pub stablecoin_program_id: String,
pub owner_id: String,
pub position_nonce: String,
pub position: AccountRead,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InitializeProgramPlanRequest {
Expand Down
Loading
Loading