use serde::{Deserialize, Serialize};
use crate::{Address, Timestamp};
pub type TaxClaimType = String;
pub type PolicyId = [u8; 32];
pub type ClaimId = [u8; 32];
pub type ProofId = [u8; 32];
pub const TAX_SCHEMA_DOMAIN_SEP: &[u8] = b"SRC821-SCHEMA:";
pub const TAX_POLICY_DOMAIN_SEP: &[u8] = b"SRC823-POLICY:";
pub const TAX_PROOF_DOMAIN_SEP: &[u8] = b"SRC824-PROOF:";
pub const TAX_DISCLOSURE_DOMAIN_SEP: &[u8] = b"SRC825-DISCLOSURE-v1";
pub const TAX_CLAIM_COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC82X-CLAIM-COMMITMENT-v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxRiskLevel {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl TaxRiskLevel {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(TaxRiskLevel::Low),
1 => Some(TaxRiskLevel::Medium),
2 => Some(TaxRiskLevel::High),
3 => Some(TaxRiskLevel::Critical),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
TaxRiskLevel::Low => "Low",
TaxRiskLevel::Medium => "Medium",
TaxRiskLevel::High => "High",
TaxRiskLevel::Critical => "Critical",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ClaimTypeStatus {
Active = 0,
Deprecated = 1,
Retired = 2,
}
impl ClaimTypeStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ClaimTypeStatus::Active),
1 => Some(ClaimTypeStatus::Deprecated),
2 => Some(ClaimTypeStatus::Retired),
_ => None,
}
}
pub fn is_usable(&self) -> bool {
matches!(self, ClaimTypeStatus::Active | ClaimTypeStatus::Deprecated)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxClaimTypeEntry {
pub claim_type: TaxClaimType,
pub schema_hash: [u8; 32],
pub risk_level: TaxRiskLevel,
pub recommended_validity_secs: u64,
pub required_issuer_classes: Vec<Vec<TaxIssuerClass>>,
pub status: ClaimTypeStatus,
pub version: u32,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
impl TaxClaimTypeEntry {
pub fn generate_schema_hash(claim_type: &str, version: u32) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(TAX_SCHEMA_DOMAIN_SEP);
hasher.update(claim_type.as_bytes());
hasher.update(b":v");
hasher.update(&version.to_string().as_bytes());
*hasher.finalize().as_bytes()
}
pub fn check_issuer_classes(&self, issuer_classes: &[TaxIssuerClass]) -> bool {
for group in &self.required_issuer_classes {
let group_satisfied = group.iter().all(|required| issuer_classes.contains(required));
if group_satisfied {
return true;
}
}
false
}
}
pub mod v1_claim_types {
use super::*;
pub const TAX_FILED_RETURN: &str = "tax.filed.return";
pub const TAX_PAID_STATUS: &str = "tax.paid.status";
pub const TAX_BALANCE_STATUS: &str = "tax.balance.status";
pub const TAX_INCOME_BRACKET: &str = "tax.income.bracket";
pub const TAX_WITHHOLDING_BRACKET: &str = "tax.withholding.bracket";
pub const TAX_NOTICE_OPEN: &str = "tax.notice.open";
pub const TAX_GOOD_STANDING: &str = "tax.good_standing";
pub fn tax_filed_return_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_FILED_RETURN.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_FILED_RETURN, 1),
risk_level: TaxRiskLevel::Medium,
recommended_validity_secs: 365 * 24 * 60 * 60, required_issuer_classes: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::AuditorCpa, TaxIssuerClass::TaxFilingProvider],
],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_paid_status_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_PAID_STATUS.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_PAID_STATUS, 1),
risk_level: TaxRiskLevel::Medium,
recommended_validity_secs: 90 * 24 * 60 * 60, required_issuer_classes: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::BankBroker],
],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_balance_status_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_BALANCE_STATUS.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_BALANCE_STATUS, 1),
risk_level: TaxRiskLevel::High,
recommended_validity_secs: 30 * 24 * 60 * 60, required_issuer_classes: vec![vec![TaxIssuerClass::TaxAuthority]],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_income_bracket_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_INCOME_BRACKET.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_INCOME_BRACKET, 1),
risk_level: TaxRiskLevel::High,
recommended_validity_secs: 365 * 24 * 60 * 60, required_issuer_classes: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::AuditorCpa],
],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_withholding_bracket_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_WITHHOLDING_BRACKET.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_WITHHOLDING_BRACKET, 1),
risk_level: TaxRiskLevel::Medium,
recommended_validity_secs: 365 * 24 * 60 * 60, required_issuer_classes: vec![
vec![TaxIssuerClass::EmployerPayroll],
vec![TaxIssuerClass::TaxAuthority],
],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_notice_open_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_NOTICE_OPEN.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_NOTICE_OPEN, 1),
risk_level: TaxRiskLevel::High,
recommended_validity_secs: 30 * 24 * 60 * 60, required_issuer_classes: vec![vec![TaxIssuerClass::TaxAuthority]],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn tax_good_standing_entry(now: Timestamp) -> TaxClaimTypeEntry {
TaxClaimTypeEntry {
claim_type: TAX_GOOD_STANDING.to_string(),
schema_hash: TaxClaimTypeEntry::generate_schema_hash(TAX_GOOD_STANDING, 1),
risk_level: TaxRiskLevel::Medium,
recommended_validity_secs: 90 * 24 * 60 * 60, required_issuer_classes: vec![vec![TaxIssuerClass::TaxAuthority]],
status: ClaimTypeStatus::Active,
version: 1,
created_at: now,
updated_at: now,
}
}
pub fn all_v1_entries(now: Timestamp) -> Vec<TaxClaimTypeEntry> {
vec![
tax_filed_return_entry(now),
tax_paid_status_entry(now),
tax_balance_status_entry(now),
tax_income_bracket_entry(now),
tax_withholding_bracket_entry(now),
tax_notice_open_entry(now),
tax_good_standing_entry(now),
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxIssuerClass {
TaxAuthority = 0,
EmployerPayroll = 1,
BankBroker = 2,
AuditorCpa = 3,
TaxFilingProvider = 4,
}
impl TaxIssuerClass {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(TaxIssuerClass::TaxAuthority),
1 => Some(TaxIssuerClass::EmployerPayroll),
2 => Some(TaxIssuerClass::BankBroker),
3 => Some(TaxIssuerClass::AuditorCpa),
4 => Some(TaxIssuerClass::TaxFilingProvider),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
TaxIssuerClass::TaxAuthority => "Tax Authority",
TaxIssuerClass::EmployerPayroll => "Employer Payroll",
TaxIssuerClass::BankBroker => "Bank/Broker",
TaxIssuerClass::AuditorCpa => "Auditor/CPA",
TaxIssuerClass::TaxFilingProvider => "Tax Filing Provider",
}
}
pub fn trust_level(&self) -> u8 {
match self {
TaxIssuerClass::TaxAuthority => 5, TaxIssuerClass::AuditorCpa => 4,
TaxIssuerClass::BankBroker => 3,
TaxIssuerClass::EmployerPayroll => 2,
TaxIssuerClass::TaxFilingProvider => 1, }
}
pub fn can_issue(&self, claim_type: &str) -> bool {
match self {
TaxIssuerClass::TaxAuthority => true, TaxIssuerClass::EmployerPayroll => {
matches!(
claim_type,
"tax.withholding.bracket" | "tax.paid.status"
)
}
TaxIssuerClass::BankBroker => {
matches!(claim_type, "tax.paid.status" | "tax.withholding.bracket")
}
TaxIssuerClass::AuditorCpa => {
matches!(claim_type, "tax.filed.return" | "tax.income.bracket")
}
TaxIssuerClass::TaxFilingProvider => {
matches!(claim_type, "tax.filed.return")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxIssuerStatus {
Active = 0,
Suspended = 1,
Revoked = 2,
Expired = 3,
}
impl TaxIssuerStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(TaxIssuerStatus::Active),
1 => Some(TaxIssuerStatus::Suspended),
2 => Some(TaxIssuerStatus::Revoked),
3 => Some(TaxIssuerStatus::Expired),
_ => None,
}
}
pub fn is_valid(&self) -> bool {
matches!(self, TaxIssuerStatus::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxIssuer {
pub address: Address,
pub tax_class: TaxIssuerClass,
pub jurisdictions: Vec<String>,
pub attributes_hash: [u8; 32],
pub attributes_schema_hash: [u8; 32],
pub registered_at: Timestamp,
pub updated_at: Timestamp,
pub status: TaxIssuerStatus,
pub expires_at: Option<Timestamp>,
}
impl TaxIssuer {
pub fn is_authorized_for_jurisdiction(&self, jurisdiction: &str) -> bool {
if self.jurisdictions.is_empty() {
return true;
}
self.jurisdictions.iter().any(|j| {
j == jurisdiction || jurisdiction.starts_with(&format!("{}-", j))
})
}
pub fn can_issue_in_jurisdiction(&self, claim_type: &str, jurisdiction: &str) -> bool {
self.status.is_valid()
&& self.tax_class.can_issue(claim_type)
&& self.is_authorized_for_jurisdiction(jurisdiction)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum QuorumRule {
Any = 0,
All = 1,
AtLeast(u8) = 2,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuerRequirements {
pub groups: Vec<Vec<TaxIssuerClass>>,
pub quorum: QuorumRule,
}
impl IssuerRequirements {
pub fn is_satisfied(&self, issuer_classes: &[TaxIssuerClass]) -> bool {
let satisfied_groups: Vec<bool> = self
.groups
.iter()
.map(|group| group.iter().all(|c| issuer_classes.contains(c)))
.collect();
match self.quorum {
QuorumRule::Any => satisfied_groups.iter().any(|&s| s),
QuorumRule::All => satisfied_groups.iter().all(|&s| s),
QuorumRule::AtLeast(n) => {
satisfied_groups.iter().filter(|&&s| s).count() >= n as usize
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxPolicyTemplate {
Filed = 0,
IncomeBracket = 1,
NoBalance = 2,
GoodStanding = 3,
}
impl TaxPolicyTemplate {
pub fn name(&self) -> &'static str {
match self {
TaxPolicyTemplate::Filed => "P-Filed",
TaxPolicyTemplate::IncomeBracket => "P-IncomeBracket",
TaxPolicyTemplate::NoBalance => "P-NoBalance",
TaxPolicyTemplate::GoodStanding => "P-GoodStanding",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxPolicy {
pub policy_id: PolicyId,
pub template: TaxPolicyTemplate,
pub claim_types: Vec<TaxClaimType>,
pub issuer_requirements: IssuerRequirements,
pub jurisdictions: Vec<String>,
pub tax_years: Vec<u32>,
pub max_age_secs: u64,
pub revocation_check: bool,
pub creator: Address,
pub created_at: Timestamp,
}
impl TaxPolicy {
pub fn generate_policy_id(
template: TaxPolicyTemplate,
jurisdictions: &[String],
tax_years: &[u32],
params_hash: &[u8; 32],
) -> PolicyId {
let mut hasher = blake3::Hasher::new();
hasher.update(TAX_POLICY_DOMAIN_SEP);
hasher.update(template.name().as_bytes());
hasher.update(b":");
let mut j_hasher = blake3::Hasher::new();
for j in jurisdictions {
j_hasher.update(j.as_bytes());
j_hasher.update(b",");
}
hasher.update(j_hasher.finalize().as_bytes());
hasher.update(b":");
let mut y_hasher = blake3::Hasher::new();
for y in tax_years {
y_hasher.update(&y.to_be_bytes());
}
hasher.update(y_hasher.finalize().as_bytes());
hasher.update(b":");
hasher.update(params_hash);
*hasher.finalize().as_bytes()
}
pub fn create_p_filed(
jurisdictions: Vec<String>,
tax_years: Vec<u32>,
max_age_days: u32,
creator: Address,
now: Timestamp,
) -> Self {
let params_hash = blake3::hash(&max_age_days.to_be_bytes());
let policy_id = Self::generate_policy_id(
TaxPolicyTemplate::Filed,
&jurisdictions,
&tax_years,
params_hash.as_bytes(),
);
TaxPolicy {
policy_id,
template: TaxPolicyTemplate::Filed,
claim_types: vec![v1_claim_types::TAX_FILED_RETURN.to_string()],
issuer_requirements: IssuerRequirements {
groups: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::AuditorCpa, TaxIssuerClass::TaxFilingProvider],
],
quorum: QuorumRule::Any,
},
jurisdictions,
tax_years,
max_age_secs: max_age_days as u64 * 86400,
revocation_check: true,
creator,
created_at: now,
}
}
pub fn create_p_income_bracket(
jurisdictions: Vec<String>,
tax_year: u32,
max_age_days: u32,
creator: Address,
now: Timestamp,
) -> Self {
let params_hash = blake3::hash(&[max_age_days.to_be_bytes(), tax_year.to_be_bytes()].concat());
let policy_id = Self::generate_policy_id(
TaxPolicyTemplate::IncomeBracket,
&jurisdictions,
&[tax_year],
params_hash.as_bytes(),
);
TaxPolicy {
policy_id,
template: TaxPolicyTemplate::IncomeBracket,
claim_types: vec![v1_claim_types::TAX_INCOME_BRACKET.to_string()],
issuer_requirements: IssuerRequirements {
groups: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::AuditorCpa],
],
quorum: QuorumRule::Any,
},
jurisdictions,
tax_years: vec![tax_year],
max_age_secs: max_age_days as u64 * 86400,
revocation_check: true,
creator,
created_at: now,
}
}
pub fn create_p_no_balance(
jurisdictions: Vec<String>,
max_age_days: u32,
creator: Address,
now: Timestamp,
) -> Self {
let params_hash = blake3::hash(&max_age_days.to_be_bytes());
let policy_id = Self::generate_policy_id(
TaxPolicyTemplate::NoBalance,
&jurisdictions,
&[],
params_hash.as_bytes(),
);
TaxPolicy {
policy_id,
template: TaxPolicyTemplate::NoBalance,
claim_types: vec![v1_claim_types::TAX_BALANCE_STATUS.to_string()],
issuer_requirements: IssuerRequirements {
groups: vec![vec![TaxIssuerClass::TaxAuthority]],
quorum: QuorumRule::Any,
},
jurisdictions,
tax_years: vec![],
max_age_secs: max_age_days as u64 * 86400,
revocation_check: true,
creator,
created_at: now,
}
}
pub fn create_p_good_standing(
jurisdictions: Vec<String>,
max_age_days: u32,
creator: Address,
now: Timestamp,
) -> Self {
let params_hash = blake3::hash(&max_age_days.to_be_bytes());
let policy_id = Self::generate_policy_id(
TaxPolicyTemplate::GoodStanding,
&jurisdictions,
&[],
params_hash.as_bytes(),
);
TaxPolicy {
policy_id,
template: TaxPolicyTemplate::GoodStanding,
claim_types: vec![v1_claim_types::TAX_GOOD_STANDING.to_string()],
issuer_requirements: IssuerRequirements {
groups: vec![vec![TaxIssuerClass::TaxAuthority]],
quorum: QuorumRule::Any,
},
jurisdictions,
tax_years: vec![],
max_age_secs: max_age_days as u64 * 86400,
revocation_check: true,
creator,
created_at: now,
}
}
}
pub mod proof_profiles {
pub const TAX_PROVE_FILED: &str = "tax.prove_filed.v1";
pub const TAX_PROVE_INCOME_BRACKET: &str = "tax.prove_income_bracket.v1";
pub const TAX_PROVE_NO_BALANCE: &str = "tax.prove_no_balance.v1";
pub const TAX_PROVE_GOOD_STANDING: &str = "tax.prove_good_standing.v1";
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxFiledPublicInputs {
pub year: u32,
pub jurisdiction: String,
pub status_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncomeBracketPublicInputs {
pub bracket_id: u32,
pub year: u32,
pub jurisdiction: String,
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoBalancePublicInputs {
pub period_end: u32,
pub jurisdiction: String,
pub balance_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoodStandingPublicInputs {
pub period_start: u32,
pub period_end: u32,
pub jurisdiction: String,
pub standing_commitment: [u8; 32],
pub proof_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxProofEnvelope {
pub proof_id: ProofId,
pub profile_id: String,
pub policy_ids: Vec<PolicyId>,
pub claim_ids: Vec<ClaimId>,
pub public_inputs: Vec<u8>,
pub proof_data: Vec<u8>,
pub proof_type: TaxProofType,
pub subject_nullifier: [u8; 32],
pub generated_at: Timestamp,
pub expires_at: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxProofType {
Mock = 0,
Groth16 = 1,
Plonk = 2,
Signature = 3,
}
impl TaxProofEnvelope {
pub fn generate_proof_id(
profile_id: &str,
public_inputs: &[u8],
nonce: &[u8; 32],
) -> ProofId {
let mut hasher = blake3::Hasher::new();
hasher.update(TAX_PROOF_DOMAIN_SEP);
hasher.update(profile_id.as_bytes());
hasher.update(b":");
hasher.update(public_inputs);
hasher.update(nonce);
*hasher.finalize().as_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxVerificationResult {
pub valid: bool,
pub profile_id: String,
pub policy_compliant: bool,
pub revocation_status: TaxRevocationCheckResult,
pub verified_at: Timestamp,
pub verifier: Option<Address>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxRevocationCheckResult {
pub checked: bool,
pub revoked: bool,
pub revocation_reason: Option<TaxRevocationReason>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxRevocationReason {
Unspecified = 0,
Superseded = 1,
Fraud = 2,
Amended = 3,
AuditAdjustment = 4,
IssuerRevoked = 5,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum EncryptionAlgorithm {
ChaCha20Poly1305 = 0,
Aes256Gcm = 1,
X25519ChaCha = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum DisclosureContentType {
TaxReturn = 0,
W2Form = 1,
Form1099 = 2,
Transcript = 3,
PaymentReceipt = 4,
AssessmentNotice = 5,
Other = 255,
}
impl DisclosureContentType {
pub fn name(&self) -> &'static str {
match self {
DisclosureContentType::TaxReturn => "Tax Return",
DisclosureContentType::W2Form => "W-2 Form",
DisclosureContentType::Form1099 => "Form 1099",
DisclosureContentType::Transcript => "Tax Transcript",
DisclosureContentType::PaymentReceipt => "Payment Receipt",
DisclosureContentType::AssessmentNotice => "Assessment Notice",
DisclosureContentType::Other => "Other",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EncryptionMeta {
pub algorithm: EncryptionAlgorithm,
pub key_hint: [u8; 32],
pub iv: Option<[u8; 12]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxDisclosureEnvelope {
pub payload_hash: [u8; 32],
pub payload_size: u64,
pub hint_uri: Option<String>,
pub encryption_meta: Option<EncryptionMeta>,
pub content_type: DisclosureContentType,
pub claim_id: Option<ClaimId>,
pub proof_id: Option<ProofId>,
pub created_at: Timestamp,
}
impl TaxDisclosureEnvelope {
pub fn generate_commitment(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(TAX_DISCLOSURE_DOMAIN_SEP);
hasher.update(&self.payload_hash);
hasher.update(&[self.content_type as u8]);
if let Some(ref claim_id) = self.claim_id {
hasher.update(claim_id);
}
if let Some(ref proof_id) = self.proof_id {
hasher.update(proof_id);
}
*hasher.finalize().as_bytes()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum TaxOperation {
RegisterClaimType = 0,
UpdateClaimType = 1,
DeprecateClaimType = 2,
RegisterIssuer = 10,
UpdateIssuer = 11,
SuspendIssuer = 12,
RevokeIssuer = 13,
CreatePolicy = 20,
UpdatePolicy = 21,
IssueClaim = 30,
RevokeClaim = 31,
VerifyProof = 40,
AttachDisclosure = 50,
}
impl TaxOperation {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(TaxOperation::RegisterClaimType),
1 => Some(TaxOperation::UpdateClaimType),
2 => Some(TaxOperation::DeprecateClaimType),
10 => Some(TaxOperation::RegisterIssuer),
11 => Some(TaxOperation::UpdateIssuer),
12 => Some(TaxOperation::SuspendIssuer),
13 => Some(TaxOperation::RevokeIssuer),
20 => Some(TaxOperation::CreatePolicy),
21 => Some(TaxOperation::UpdatePolicy),
30 => Some(TaxOperation::IssueClaim),
31 => Some(TaxOperation::RevokeClaim),
40 => Some(TaxOperation::VerifyProof),
50 => Some(TaxOperation::AttachDisclosure),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaxTxData {
pub operation: TaxOperation,
pub data: Vec<u8>,
pub recipient: crate::Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxRegistryEvent {
ClaimTypeAdded {
claim_type: TaxClaimType,
schema_hash: [u8; 32],
version: u32,
},
ClaimTypeUpdated {
claim_type: TaxClaimType,
schema_hash: [u8; 32],
old_version: u32,
new_version: u32,
},
ClaimTypeDeprecated {
claim_type: TaxClaimType,
version: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxIssuerEvent {
IssuerRegistered {
address: Address,
tax_class: TaxIssuerClass,
jurisdictions: Vec<String>,
},
IssuerUpdated {
address: Address,
},
IssuerStatusChanged {
address: Address,
old_status: TaxIssuerStatus,
new_status: TaxIssuerStatus,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxPolicyEvent {
PolicyCreated {
policy_id: PolicyId,
template: TaxPolicyTemplate,
creator: Address,
},
PolicyUpdated {
policy_id: PolicyId,
updater: Address,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxProofEvent {
ProofVerified {
proof_id: ProofId,
profile_id: String,
policy_ids: Vec<PolicyId>,
verifier: Address,
valid: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxDisclosureEvent {
DisclosureAttached {
disclosure_commitment: [u8; 32],
claim_id: Option<ClaimId>,
proof_id: Option<ProofId>,
content_type: DisclosureContentType,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaxEvent {
Registry(TaxRegistryEvent),
Issuer(TaxIssuerEvent),
Policy(TaxPolicyEvent),
Proof(TaxProofEvent),
Disclosure(TaxDisclosureEvent),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_claim_type_schema_hash() {
let hash1 = TaxClaimTypeEntry::generate_schema_hash("tax.filed.return", 1);
let hash2 = TaxClaimTypeEntry::generate_schema_hash("tax.filed.return", 1);
let hash3 = TaxClaimTypeEntry::generate_schema_hash("tax.filed.return", 2);
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_issuer_class_authorization() {
let authority = TaxIssuerClass::TaxAuthority;
let cpa = TaxIssuerClass::AuditorCpa;
let filing_provider = TaxIssuerClass::TaxFilingProvider;
assert!(authority.can_issue("tax.filed.return"));
assert!(authority.can_issue("tax.income.bracket"));
assert!(authority.can_issue("tax.balance.status"));
assert!(cpa.can_issue("tax.filed.return"));
assert!(cpa.can_issue("tax.income.bracket"));
assert!(!cpa.can_issue("tax.balance.status"));
assert!(filing_provider.can_issue("tax.filed.return"));
assert!(!filing_provider.can_issue("tax.income.bracket"));
}
#[test]
fn test_issuer_requirements_satisfaction() {
let requirements = IssuerRequirements {
groups: vec![
vec![TaxIssuerClass::TaxAuthority],
vec![TaxIssuerClass::AuditorCpa, TaxIssuerClass::TaxFilingProvider],
],
quorum: QuorumRule::Any,
};
assert!(requirements.is_satisfied(&[TaxIssuerClass::TaxAuthority]));
assert!(requirements.is_satisfied(&[
TaxIssuerClass::AuditorCpa,
TaxIssuerClass::TaxFilingProvider
]));
assert!(!requirements.is_satisfied(&[TaxIssuerClass::AuditorCpa]));
assert!(!requirements.is_satisfied(&[TaxIssuerClass::TaxFilingProvider]));
}
#[test]
fn test_policy_id_determinism() {
let creator = Address::ZERO;
let now = 1704067200000u64;
let policy1 = TaxPolicy::create_p_filed(
vec!["US".to_string()],
vec![2023],
365,
creator,
now,
);
let policy2 = TaxPolicy::create_p_filed(
vec!["US".to_string()],
vec![2023],
365,
creator,
now,
);
let policy3 = TaxPolicy::create_p_filed(
vec!["CA".to_string()],
vec![2023],
365,
creator,
now,
);
assert_eq!(policy1.policy_id, policy2.policy_id);
assert_ne!(policy1.policy_id, policy3.policy_id);
}
#[test]
fn test_v1_claim_types() {
let now = 1704067200000u64;
let entries = v1_claim_types::all_v1_entries(now);
assert_eq!(entries.len(), 7);
let mut claim_types: Vec<_> = entries.iter().map(|e| &e.claim_type).collect();
claim_types.sort();
claim_types.dedup();
assert_eq!(claim_types.len(), 7);
}
#[test]
fn test_disclosure_commitment() {
let disclosure = TaxDisclosureEnvelope {
payload_hash: [1u8; 32],
payload_size: 1024,
hint_uri: Some("ipfs://Qm...".to_string()),
encryption_meta: None,
content_type: DisclosureContentType::TaxReturn,
claim_id: Some([2u8; 32]),
proof_id: None,
created_at: 1704067200000,
};
let commitment1 = disclosure.generate_commitment();
let commitment2 = disclosure.generate_commitment();
assert_eq!(commitment1, commitment2);
let mut disclosure2 = disclosure.clone();
disclosure2.payload_hash = [3u8; 32];
let commitment3 = disclosure2.generate_commitment();
assert_ne!(commitment1, commitment3);
}
#[test]
fn test_tax_issuer_jurisdiction_check() {
let issuer = TaxIssuer {
address: Address::ZERO,
tax_class: TaxIssuerClass::TaxAuthority,
jurisdictions: vec!["US".to_string()],
attributes_hash: [0u8; 32],
attributes_schema_hash: [0u8; 32],
registered_at: 1704067200000,
updated_at: 1704067200000,
status: TaxIssuerStatus::Active,
expires_at: None,
};
assert!(issuer.is_authorized_for_jurisdiction("US"));
assert!(issuer.is_authorized_for_jurisdiction("US-CA"));
assert!(issuer.is_authorized_for_jurisdiction("US-NY"));
assert!(!issuer.is_authorized_for_jurisdiction("CA"));
assert!(!issuer.is_authorized_for_jurisdiction("GB"));
}
}