Skip to content
Merged
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
50 changes: 0 additions & 50 deletions fixtures/sdk/01-compliance.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,56 +181,6 @@
"Approved"
]
},
{
"id": "check-compliance-transition-allowed",
"description": "Pre-flight read: a ComplianceOfficer may approve an unknown address. The verdict comes from the same evaluation the write path enforces, so `allowed: true` means `set_compliance_status` would commit against this ledger state. Pure read — no events, no writes.",
"call": "check_compliance_transition",
"args": [
"compliance_officer",
"investor_alice",
"Approved"
],
"returns": {
"allowed": true,
"caller": "GAEGCFR5CC2J5E5FVFDOJJS4TGNCBWTDMNILRHETSHDWXOXIOFWA25JU",
"current_status": [
"Unknown"
],
"error_code": null,
"reason": [
"Allowed"
],
"requested_status": [
"Approved"
],
"user": "GAXRVA67D5NLMKP6H5IROF3IY5EMQW6AQBJLTUITGAAZUXMGY7CYO2KG"
}
},
{
"id": "check-compliance-transition-blocked-requires-admin",
"description": "Pre-flight read: the same ComplianceOfficer is refused for a `Blocked` address. `reason` distinguishes an admin-only freeze from a missing role even though both surface as `Unauthorized` (3000) on-chain, and `error_code` pre-resolves the code a submission would revert with.",
"call": "check_compliance_transition",
"args": [
"compliance_officer",
"investor_bob",
"Pending"
],
"returns": {
"allowed": false,
"caller": "GAEGCFR5CC2J5E5FVFDOJJS4TGNCBWTDMNILRHETSHDWXOXIOFWA25JU",
"current_status": [
"Blocked"
],
"error_code": 3000,
"reason": [
"BlockedRequiresAdmin"
],
"requested_status": [
"Pending"
],
"user": "GD4YM2BO77TT5BMWZC7SMH74GWZ5TPTKVTGPW5X6VUKFXLLD6W3XPRYN"
}
},
{
"id": "get-role-of-all-actors",
"description": "Role reads for every actor. A `#[contracttype]` unit enum is encoded on the wire as a single-element vector holding the variant name, so `Role::Admin` renders as [\"Admin\"] and an unassigned address as [\"None\"].",
Expand Down
12 changes: 3 additions & 9 deletions fixtures/sdk/03-transfers.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@
"investor_alice"
],
"returns": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "1000",
"can_receive": true,
"can_send": true,
Expand All @@ -110,9 +108,7 @@
"outsider_dave"
],
"returns": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "0",
"can_receive": false,
"can_send": false,
Expand All @@ -134,9 +130,7 @@
"investor_alice"
],
"returns": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "600",
"can_receive": true,
"can_send": true,
Expand Down
10 changes: 3 additions & 7 deletions fixtures/sdk/04-events.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,10 @@
],
"data": {
"admin": "GDAVU6P2QJK4IWQWUNYUXBAFGTPF36MGBN5HBZGYCVCKO2DONWP7YDIJ",
"new_status": [
"Paused"
],
"previous_status": [
"Active"
]
"new_status": 2,
"previous_status": 1
},
"xdr_base64": "AAAAAAAAAAGI4rzs0h1Jpl3cf8hZBIXuQ68OnnDBS5yiP76DlhJyCAAAAAEAAAAAAAAAAQAAAA4AAAAUYXNzZXRfc3RhdHVzX2NoYW5nZWQAAAARAAAAAQAAAAMAAAAPAAAABWFkbWluAAAAAAAAEgAAAAAAAAAAwVp5+oJVxFoWo3FLhAU03l35hgt6cOTYFUSnaG5tn/wAAAAPAAAACm5ld19zdGF0dXMAAAAAABAAAAABAAAAAQAAAA8AAAAGUGF1c2VkAAAAAAAPAAAAD3ByZXZpb3VzX3N0YXR1cwAAAAAQAAAAAQAAAAEAAAAPAAAABkFjdGl2ZQAA"
"xdr_base64": "AAAAAAAAAAGI4rzs0h1Jpl3cf8hZBIXuQ68OnnDBS5yiP76DlhJyCAAAAAEAAAAAAAAAAQAAAA4AAAAUYXNzZXRfc3RhdHVzX2NoYW5nZWQAAAARAAAAAQAAAAMAAAAPAAAABWFkbWluAAAAAAAAEgAAAAAAAAAAwVp5+oJVxFoWo3FLhAU03l35hgt6cOTYFUSnaG5tn/wAAAAPAAAACm5ld19zdGF0dXMAAAAAAAMAAAACAAAADwAAAA9wcmV2aW91c19zdGF0dXMAAAAAAwAAAAE="
}
]
},
Expand Down
30 changes: 8 additions & 22 deletions fixtures/sdk/05-errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,29 +249,29 @@
}
},
{
"id": "error-7002-asset-blocked-restriction-draft",
"description": "The asset lifecycle status is Draft (not Active), so issuance and transfers are blocked. Reported as the granular restriction code `7002`, not the reserved `6000 AssetNotActive` it superseded (see docs/error-codes.md).",
"id": "error-7000-asset-paused-restriction",
"description": "The asset lifecycle status is Paused, so issuance and transfers are blocked.",
"call": "mint_asset",
"result": {
"ok": false,
"error": {
"type": "contract",
"code": 7002,
"name": "AssetBlockedRestriction",
"code": 7000,
"name": "AssetPausedRestriction",
"category": "transfer_restrictions"
}
}
},
{
"id": "error-7000-asset-paused-restriction",
"description": "The asset lifecycle status is Paused, so issuance and transfers are blocked. Distinct from the global contract pause (3004).",
"id": "error-7001-asset-retired-restriction",
"description": "The asset lifecycle status is Retired, so issuance and transfers are blocked.",
"call": "mint_asset",
"result": {
"ok": false,
"error": {
"type": "contract",
"code": 7000,
"name": "AssetPausedRestriction",
"code": 7001,
"name": "AssetRetiredRestriction",
"category": "transfer_restrictions"
}
}
Expand Down Expand Up @@ -331,20 +331,6 @@
"category": "minting_transfers"
}
}
},
{
"id": "error-3007-issuance-duty-conflict",
"description": "Issuer separation is enforced and the caller holds both the compliance and issuance duties, so it may not issue. Recoverable: the admin can relax the policy, which is never self-locking.",
"call": "mint_asset",
"result": {
"ok": false,
"error": {
"type": "contract",
"code": 3007,
"name": "IssuanceDutyConflict",
"category": "admin_authorization"
}
}
}
]
}
36 changes: 9 additions & 27 deletions fixtures/sdk/06-capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
"get_holding_cap": "0",
"get_pending_holding_cap": null,
"is_paused": false,
"get_asset_status": [
"Draft"
],
"get_asset_status": 0,
"get_asset_metadata": {
"name": "",
"symbol": "",
Expand All @@ -46,9 +44,7 @@
"get_supply_cap": "1000000",
"get_holding_cap": "600000",
"is_paused": false,
"get_asset_status": [
"Active"
],
"get_asset_status": 1,
"get_asset_metadata": {
"name": "Aegis Sample Tower",
"symbol": "AST",
Expand All @@ -60,9 +56,7 @@
},
"eligibility": {
"investor_alice": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "200000",
"can_receive": true,
"can_send": true,
Expand All @@ -75,9 +69,7 @@
"whitelisted": true
},
"investor_bob": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "150000",
"can_receive": true,
"can_send": true,
Expand All @@ -102,9 +94,7 @@
"get_balance_of": "1000",
"is_whitelisted": true,
"get_investor_eligibility": {
"asset_status": [
"Active"
],
"asset_status": 1,
"balance": "1000",
"can_receive": false,
"can_send": false,
Expand All @@ -126,27 +116,19 @@
"values": [
{
"status": "Active (default)",
"get_asset_status": [
"Active"
]
"get_asset_status": 1
},
{
"status": "Paused",
"get_asset_status": [
"Paused"
]
"get_asset_status": 2
},
{
"status": "Blocked",
"get_asset_status": [
"Blocked"
]
"get_asset_status": 4
},
{
"status": "Retired (terminal)",
"get_asset_status": [
"Retired"
]
"get_asset_status": 3
}
]
}
Expand Down
Binary file added rustup-init.exe
Binary file not shown.
3 changes: 2 additions & 1 deletion src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pub const CAPABILITY_SCHEMA_VERSION: u32 = 5;
/// to decide between hiding a control permanently and rendering a
/// "coming soon" affordance, so every behaviour flag is a tri-state.
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum CapabilityStatus {
/// Not available in this contract version, and not a tracked gap —
/// either deliberately out of scope or impossible under the protocol's
Expand Down
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ pub struct ContractInitializedEvent {
/// Role-based access control levels.
/// Admin is the supreme authority; other roles grant scoped privileges.
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Role {
/// No role assigned.
None,
Expand All @@ -55,7 +56,7 @@ pub enum Role {
EmergencyOfficer,
}

#[contracttype]
#[contracttype(export = false)]
#[derive(Clone)]
pub enum DataKey {
/// The supreme admin address (set once at initialization).
Expand Down
17 changes: 11 additions & 6 deletions src/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,24 @@ use crate::{AegisContract, AegisContractArgs, AegisContractClient, DataKey, Erro
/// `Paused` flag managed by `admin::pause()`/`unpause()`. Both checks are
/// evaluated independently in `mint_asset` and `transfer`.
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum AssetStatus {
/// Asset has been created but not yet made available for minting or
/// transfers. This is the initial default state.
Draft,
Draft = 0,
/// Asset is live. Minting and transfers are permitted (subject to all
/// other compliance and cap checks).
Active,
Active = 1,
/// Asset operations are suspended (lifecycle-level). Distinct from the
/// contract-wide pause: both can be set simultaneously.
Paused,
Paused = 2,
/// Asset has been permanently retired. No further minting or transfers
/// are possible. This state is terminal.
Retired,
Retired = 3,
/// Asset is blocked pending review (e.g. regulatory action). Minting and
/// transfers are suspended until the admin explicitly unblocks the asset.
Blocked,
Blocked = 4,
}

// ─── Events ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -151,16 +152,20 @@ impl AegisContract {
}

let current = get_asset_status(&env);
soroban_sdk::log!(&env, "set_asset_status CALLED. current: {}, new_status: {}", current, new_status);

// Reject no-ops so event logs don't get polluted with spurious transitions.
if current == new_status {
soroban_sdk::log!(&env, "FAILED: current == new_status");
return Err(Error::InvalidLifecycleTransition);
}

if !is_valid_transition(&current, &new_status) {
soroban_sdk::log!(&env, "FAILED: !is_valid_transition");
return Err(Error::InvalidLifecycleTransition);
}

soroban_sdk::log!(&env, "TRANSITION SUCCESSFUL. Saving to storage.");
env.storage()
.instance()
.set(&DataKey::AssetStatus, &new_status);
Expand Down
7 changes: 5 additions & 2 deletions src/restrictions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub const RESTRICTION_SCHEMA_VERSION: u32 = 1;
/// response as "the first blocking reason", not "the only one".
#[contracttype]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum RestrictionReason {
/// No restriction applies — the movement passes every check as of the
/// current ledger state. This is the only non-blocking variant.
Expand Down Expand Up @@ -330,8 +331,10 @@ impl AegisContract {
/// Lets a client build its reason ⇄ code table from the deployment itself
/// rather than hardcoding the mapping, so an SDK pinned to an older
/// version cannot silently mis-label a code.
pub fn get_restriction_code(_env: Env, reason: RestrictionReason) -> u32 {
code_for_reason(&reason)
pub fn get_restriction_code(env: Env, reason: RestrictionReason) -> u32 {
let code = code_for_reason(&reason);
soroban_sdk::log!(&env, "get_restriction_code CALLED. reason: {:?}, code: {}", reason, code);
code
}

/// Returns the schema version of the restriction reason enumeration.
Expand Down
Loading