Skip to content

Commit 342610c

Browse files
authored
Merge pull request #180 from johnsaviour56-ship-it/refactor/modularize-monolithic-contracts
refactor: modularize documentation and credential_registry contracts
2 parents 8838747 + c037ab1 commit 342610c

File tree

11 files changed

+526
-180
lines changed

11 files changed

+526
-180
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//! Core credential operations: issue, revoke, query, and status checks.
2+
3+
use soroban_sdk::{symbol_short, Address, Bytes, BytesN, Env};
4+
5+
use crate::events::{Crediss, Credrev};
6+
7+
/// A credential record stored on-chain:
8+
/// `(issuer_did, subject_did, metadata_ptr, expires_at, status)`
9+
type CredRecord = (Bytes, Bytes, Bytes, i128, i32);
10+
11+
pub struct CredentialManager;
12+
13+
impl CredentialManager {
14+
/// Issue a new credential.
15+
///
16+
/// Panics if a credential with the same hash already exists.
17+
pub fn issue(
18+
env: &Env,
19+
credential_hash: BytesN<32>,
20+
issuer: Address,
21+
issuer_did: Bytes,
22+
subject_did: Bytes,
23+
metadata_ptr: Bytes,
24+
expires_at: i128,
25+
) {
26+
issuer.require_auth();
27+
28+
let key = (symbol_short!("cred"), credential_hash.clone());
29+
assert!(
30+
!env.storage().persistent().has(&key),
31+
"credential already exists"
32+
);
33+
34+
let record: CredRecord = (
35+
issuer_did.clone(),
36+
subject_did.clone(),
37+
metadata_ptr.clone(),
38+
expires_at,
39+
0i32,
40+
);
41+
env.storage().persistent().set(&key, &record);
42+
43+
Crediss {
44+
credential_hash,
45+
issuer_did,
46+
subject_did,
47+
metadata_ptr,
48+
expires_at,
49+
}
50+
.publish(env);
51+
}
52+
53+
/// Revoke an existing credential.
54+
///
55+
/// Panics if the credential does not exist.
56+
pub fn revoke(env: &Env, credential_hash: BytesN<32>, issuer: Address) {
57+
issuer.require_auth();
58+
59+
let key = (symbol_short!("cred"), credential_hash.clone());
60+
let opt: Option<CredRecord> = env.storage().persistent().get(&key);
61+
62+
match opt {
63+
Some((issuer_did, subject_did, metadata_ptr, expires_at, _)) => {
64+
let record: CredRecord =
65+
(issuer_did.clone(), subject_did.clone(), metadata_ptr, expires_at, 1i32);
66+
env.storage().persistent().set(&key, &record);
67+
68+
Credrev {
69+
credential_hash,
70+
issuer_did,
71+
subject_did,
72+
}
73+
.publish(env);
74+
}
75+
None => panic!("credential not found"),
76+
}
77+
}
78+
79+
/// Retrieve a raw credential record, or `None` if not found.
80+
pub fn get(env: &Env, credential_hash: BytesN<32>) -> Option<CredRecord> {
81+
let key = (symbol_short!("cred"), credential_hash);
82+
env.storage().persistent().get(&key)
83+
}
84+
85+
/// Return `true` if the credential exists, is not revoked, and has not expired.
86+
pub fn is_active(env: &Env, credential_hash: BytesN<32>, now_ts: i128) -> bool {
87+
match Self::get(env, credential_hash) {
88+
Some((_, _, _, expires_at, status)) => {
89+
if status == 1 {
90+
return false;
91+
}
92+
if expires_at > 0 && now_ts > expires_at {
93+
return false;
94+
}
95+
true
96+
}
97+
None => false,
98+
}
99+
}
100+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//! Error definitions for the credential registry contract.
2+
3+
/// Errors that can occur during credential operations.
4+
pub enum CredentialError {
5+
/// A credential with this hash already exists.
6+
AlreadyExists,
7+
/// No credential found for the given hash.
8+
NotFound,
9+
}
10+
11+
impl CredentialError {
12+
pub fn message(&self) -> &'static str {
13+
match self {
14+
CredentialError::AlreadyExists => "credential already exists",
15+
CredentialError::NotFound => "credential not found",
16+
}
17+
}
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! On-chain events emitted by the credential registry.
2+
3+
use soroban_sdk::{contractevent, Bytes, BytesN};
4+
5+
/// Emitted when a credential is issued.
6+
#[contractevent]
7+
#[derive(Clone, Debug, Eq, PartialEq)]
8+
pub struct Crediss {
9+
pub credential_hash: BytesN<32>,
10+
pub issuer_did: Bytes,
11+
pub subject_did: Bytes,
12+
pub metadata_ptr: Bytes,
13+
pub expires_at: i128,
14+
}
15+
16+
/// Emitted when a credential is revoked.
17+
#[contractevent]
18+
#[derive(Clone, Debug, Eq, PartialEq)]
19+
pub struct Credrev {
20+
pub credential_hash: BytesN<32>,
21+
pub issuer_did: Bytes,
22+
pub subject_did: Bytes,
23+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! TeachLink Credential Registry Contract
2+
//!
3+
//! On-chain registry for verifiable credentials (VCs).
4+
//! Functionality is split across focused modules:
5+
//!
6+
//! - [`types`] — credential status enum
7+
//! - [`events`] — on-chain event definitions
8+
//! - [`errors`] — error definitions
9+
//! - [`credentials`] — issue, revoke, query, and status logic
10+
11+
#![no_std]
12+
13+
use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env};
14+
15+
mod credentials;
16+
mod errors;
17+
mod events;
18+
mod types;
19+
20+
pub use events::{Crediss, Credrev};
21+
pub use types::CredentialStatus;
22+
23+
/// Credential registry contract — delegates to focused sub-modules.
24+
#[contract]
25+
pub struct CredentialRegistryContract;
26+
27+
#[contractimpl]
28+
impl CredentialRegistryContract {
29+
/// Issue a credential by storing its hash and metadata pointer.
30+
///
31+
/// `credential_hash` should be a deterministic hash (e.g. SHA-256) of the full VC JSON.
32+
pub fn issue_credential(
33+
env: &Env,
34+
credential_hash: BytesN<32>,
35+
issuer: Address,
36+
issuer_did: Bytes,
37+
subject_did: Bytes,
38+
metadata_ptr: Bytes,
39+
expires_at: i128,
40+
) {
41+
credentials::CredentialManager::issue(
42+
env,
43+
credential_hash,
44+
issuer,
45+
issuer_did,
46+
subject_did,
47+
metadata_ptr,
48+
expires_at,
49+
);
50+
}
51+
52+
/// Revoke a credential. Caller must be the original issuer.
53+
pub fn revoke_credential(env: &Env, credential_hash: BytesN<32>, issuer: Address) {
54+
credentials::CredentialManager::revoke(env, credential_hash, issuer);
55+
}
56+
57+
/// Get a credential record: `(issuer_did, subject_did, metadata_ptr, expires_at, status)`.
58+
pub fn get_credential(
59+
env: &Env,
60+
credential_hash: BytesN<32>,
61+
) -> Option<(Bytes, Bytes, Bytes, i128, i32)> {
62+
credentials::CredentialManager::get(env, credential_hash)
63+
}
64+
65+
/// Check if a credential is active (not revoked and not expired).
66+
pub fn is_active(env: &Env, credential_hash: BytesN<32>, now_ts: i128) -> bool {
67+
credentials::CredentialManager::is_active(env, credential_hash, now_ts)
68+
}
69+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! Credential registry data types.
2+
3+
/// Status of a credential.
4+
#[derive(Clone, PartialEq)]
5+
pub enum CredentialStatus {
6+
Active,
7+
Revoked,
8+
Expired,
9+
}
10+
11+
impl CredentialStatus {
12+
/// Numeric representation stored on-chain: 0 = Active, 1 = Revoked.
13+
pub fn to_i32(status: &CredentialStatus) -> i32 {
14+
match status {
15+
CredentialStatus::Active => 0,
16+
CredentialStatus::Revoked => 1,
17+
CredentialStatus::Expired => 2,
18+
}
19+
}
20+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! Article management: create, retrieve, update, and analytics for documentation articles.
2+
3+
use soroban_sdk::{Address, Env, String, Vec};
4+
5+
use crate::storage::DocKey;
6+
use crate::types::{Article, DocCategory, Visibility};
7+
8+
pub struct ArticleManager;
9+
10+
impl ArticleManager {
11+
/// Create a new documentation article.
12+
pub fn create(
13+
env: &Env,
14+
id: String,
15+
title: String,
16+
content: String,
17+
category: DocCategory,
18+
language: String,
19+
tags: Vec<String>,
20+
visibility: Visibility,
21+
author: Address,
22+
) -> Article {
23+
let timestamp = env.ledger().timestamp();
24+
25+
let article = Article {
26+
id: id.clone(),
27+
title,
28+
content,
29+
category,
30+
language,
31+
version: 1,
32+
author,
33+
visibility,
34+
tags,
35+
created_at: timestamp,
36+
updated_at: timestamp,
37+
view_count: 0,
38+
helpful_count: 0,
39+
};
40+
41+
env.storage().instance().set(&id, &article);
42+
43+
let count: u64 = env
44+
.storage()
45+
.instance()
46+
.get(&DocKey::ArticleCount)
47+
.unwrap_or(0);
48+
env.storage()
49+
.instance()
50+
.set(&DocKey::ArticleCount, &(count + 1));
51+
52+
article
53+
}
54+
55+
/// Retrieve an article by ID.
56+
pub fn get(env: &Env, id: String) -> Article {
57+
env.storage().instance().get(&id).unwrap()
58+
}
59+
60+
/// Update title, content, and tags of an existing article.
61+
pub fn update(
62+
env: &Env,
63+
id: String,
64+
title: String,
65+
content: String,
66+
tags: Vec<String>,
67+
) -> Article {
68+
let mut article: Article = env.storage().instance().get(&id).unwrap();
69+
70+
article.title = title;
71+
article.content = content;
72+
article.tags = tags;
73+
article.version += 1;
74+
article.updated_at = env.ledger().timestamp();
75+
76+
env.storage().instance().set(&id, &article);
77+
article
78+
}
79+
80+
/// Increment the view counter for an article.
81+
pub fn record_view(env: &Env, article_id: String) {
82+
let mut article: Article = env.storage().instance().get(&article_id).unwrap();
83+
article.view_count += 1;
84+
env.storage().instance().set(&article_id, &article);
85+
}
86+
87+
/// Increment the helpful counter for an article.
88+
pub fn mark_helpful(env: &Env, article_id: String) {
89+
let mut article: Article = env.storage().instance().get(&article_id).unwrap();
90+
article.helpful_count += 1;
91+
env.storage().instance().set(&article_id, &article);
92+
}
93+
94+
/// Return total number of articles stored.
95+
pub fn count(env: &Env) -> u64 {
96+
env.storage()
97+
.instance()
98+
.get(&DocKey::ArticleCount)
99+
.unwrap_or(0)
100+
}
101+
}

contracts/documentation/src/faq.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! FAQ management: create and retrieve FAQ entries.
2+
3+
use soroban_sdk::{Address, Env, String};
4+
5+
use crate::storage::DocKey;
6+
use crate::types::FaqEntry;
7+
8+
pub struct FaqManager;
9+
10+
impl FaqManager {
11+
/// Create a new FAQ entry.
12+
pub fn create(
13+
env: &Env,
14+
id: String,
15+
question: String,
16+
answer: String,
17+
category: String,
18+
language: String,
19+
author: Address,
20+
) -> FaqEntry {
21+
let timestamp = env.ledger().timestamp();
22+
23+
let faq = FaqEntry {
24+
id: id.clone(),
25+
question,
26+
answer,
27+
category,
28+
language,
29+
author,
30+
created_at: timestamp,
31+
updated_at: timestamp,
32+
helpful_count: 0,
33+
};
34+
35+
env.storage().instance().set(&id, &faq);
36+
37+
let count: u64 = env.storage().instance().get(&DocKey::FaqCount).unwrap_or(0);
38+
env.storage()
39+
.instance()
40+
.set(&DocKey::FaqCount, &(count + 1));
41+
42+
faq
43+
}
44+
45+
/// Retrieve a FAQ entry by ID.
46+
pub fn get(env: &Env, id: String) -> FaqEntry {
47+
env.storage().instance().get(&id).unwrap()
48+
}
49+
50+
/// Return total number of FAQ entries stored.
51+
pub fn count(env: &Env) -> u64 {
52+
env.storage().instance().get(&DocKey::FaqCount).unwrap_or(0)
53+
}
54+
}

0 commit comments

Comments
 (0)