use chrono::{DateTime, SecondsFormat, Utc};
use serde_json::{Value, json};
use vta_sdk::protocols::join_requests as jr;
use vta_sdk::protocols::trust_task_reject_reasons as reasons;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChallengeTask {
Submit,
Supplement,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChallengeRefusal {
NotIssued,
AlreadyUsed,
Expired {
expired_at: DateTime<Utc>,
},
Mismatch,
}
impl ChallengeRefusal {
pub fn kind(&self) -> &'static str {
match self {
Self::NotIssued => "not_issued",
Self::AlreadyUsed => "already_used",
Self::Expired { .. } => "expired",
Self::Mismatch => "mismatch",
}
}
pub fn code(&self, task: ChallengeTask) -> &'static str {
match (task, self) {
(ChallengeTask::Submit, Self::NotIssued) => {
jr::JOIN_REQUEST_SUBMIT_ERR_CHALLENGE_NOT_ISSUED
}
(ChallengeTask::Submit, Self::AlreadyUsed) => {
jr::JOIN_REQUEST_SUBMIT_ERR_CHALLENGE_ALREADY_USED
}
(ChallengeTask::Submit, Self::Expired { .. }) => {
jr::JOIN_REQUEST_SUBMIT_ERR_CHALLENGE_EXPIRED
}
(ChallengeTask::Submit, Self::Mismatch) => {
jr::JOIN_REQUEST_SUBMIT_ERR_CHALLENGE_MISMATCH
}
(ChallengeTask::Supplement, Self::NotIssued) => {
jr::JOIN_REQUEST_SUPPLEMENT_ERR_CHALLENGE_NOT_ISSUED
}
(ChallengeTask::Supplement, Self::AlreadyUsed) => {
jr::JOIN_REQUEST_SUPPLEMENT_ERR_CHALLENGE_ALREADY_USED
}
(ChallengeTask::Supplement, Self::Expired { .. }) => {
jr::JOIN_REQUEST_SUPPLEMENT_ERR_CHALLENGE_EXPIRED
}
(ChallengeTask::Supplement, Self::Mismatch) => {
jr::JOIN_REQUEST_SUPPLEMENT_ERR_CHALLENGE_MISMATCH
}
}
}
pub fn marker(&self) -> &'static str {
match self {
Self::NotIssued => reasons::NOT_FOUND,
Self::AlreadyUsed | Self::Expired { .. } => reasons::GONE,
Self::Mismatch => reasons::CONFLICT,
}
}
pub fn details(&self) -> Option<Value> {
match self {
Self::Expired { expired_at } => Some(json!({
"expiredAt": expired_at.to_rfc3339_opts(SecondsFormat::Secs, true),
})),
_ => None,
}
}
}
const ASK_AGAIN: &str = "ask for a new one with vtc/vetting/pcs-challenge/0.1, build the proof \
over it, and submit again";
impl std::fmt::Display for ChallengeRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotIssued => write!(
f,
"this community holds no hidden-vetting challenge for you — none was asked for, \
or it expired and was cleared; {ASK_AGAIN}"
),
Self::AlreadyUsed => write!(
f,
"the hidden-vetting challenge this proof is bound to was already used by an \
earlier submission, and each is accepted once; {ASK_AGAIN}"
),
Self::Expired { expired_at } => write!(
f,
"the hidden-vetting challenge this proof is bound to expired at {}; {ASK_AGAIN}",
expired_at.to_rfc3339_opts(SecondsFormat::Secs, true)
),
Self::Mismatch => write!(
f,
"the proof is bound to a different hidden-vetting challenge from the one this \
community issued you — usually an older one, replaced when a newer challenge \
was asked for. The challenge it held is spent by this attempt; {ASK_AGAIN}"
),
}
}
}
impl std::error::Error for ChallengeRefusal {}
impl From<ChallengeRefusal> for vti_common::error::AppError {
fn from(r: ChallengeRefusal) -> Self {
Self::Validation(r.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all() -> [ChallengeRefusal; 4] {
[
ChallengeRefusal::NotIssued,
ChallengeRefusal::AlreadyUsed,
ChallengeRefusal::Expired {
expired_at: "2026-09-23T10:15:01Z".parse().unwrap(),
},
ChallengeRefusal::Mismatch,
]
}
#[test]
fn each_refusal_has_its_own_code_under_each_task() {
let expected = [
(
"vtc/join-requests/submit:challengeNotIssued",
"vtc/join-requests/supplement:challengeNotIssued",
),
(
"vtc/join-requests/submit:challengeAlreadyUsed",
"vtc/join-requests/supplement:challengeAlreadyUsed",
),
(
"vtc/join-requests/submit:challengeExpired",
"vtc/join-requests/supplement:challengeExpired",
),
(
"vtc/join-requests/submit:challengeMismatch",
"vtc/join-requests/supplement:challengeMismatch",
),
];
for (r, (submit, supplement)) in all().iter().zip(expected) {
assert_eq!(r.code(ChallengeTask::Submit), submit, "{r:?}");
assert_eq!(r.code(ChallengeTask::Supplement), supplement, "{r:?}");
}
}
#[test]
fn each_refusal_carries_the_marker_a_code_unaware_client_reads() {
let markers: Vec<_> = all().iter().map(ChallengeRefusal::marker).collect();
assert_eq!(
markers,
[
reasons::NOT_FOUND,
reasons::GONE,
reasons::GONE,
reasons::CONFLICT
]
);
}
#[test]
fn only_an_expired_refusal_says_when() {
for r in all() {
match &r {
ChallengeRefusal::Expired { .. } => assert_eq!(
r.details(),
Some(json!({ "expiredAt": "2026-09-23T10:15:01Z" }))
),
_ => assert_eq!(r.details(), None, "{r:?}"),
}
}
}
#[test]
fn every_message_names_the_remedy_and_every_kind_is_distinct() {
let mut kinds = std::collections::BTreeSet::new();
for r in all() {
assert!(
r.to_string().contains("vtc/vetting/pcs-challenge/0.1"),
"{r}"
);
assert!(kinds.insert(r.kind()), "{}", r.kind());
}
}
}