diff --git a/BiatecTokensApi/Controllers/TokenStandardsController.cs b/BiatecTokensApi/Controllers/TokenStandardsController.cs
new file mode 100644
index 0000000..4a16c0d
--- /dev/null
+++ b/BiatecTokensApi/Controllers/TokenStandardsController.cs
@@ -0,0 +1,235 @@
+using BiatecTokensApi.Filters;
+using BiatecTokensApi.Helpers;
+using BiatecTokensApi.Models;
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services.Interface;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace BiatecTokensApi.Controllers
+{
+ ///
+ /// Controller for token standard compliance and validation endpoints
+ ///
+ [Authorize]
+ [ApiController]
+ [Route("api/v1/standards")]
+ public class TokenStandardsController : ControllerBase
+ {
+ private readonly ITokenStandardRegistry _registry;
+ private readonly ITokenStandardValidator _validator;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the TokenStandardsController
+ ///
+ /// Token standard registry service
+ /// Token standard validator service
+ /// Logger instance
+ public TokenStandardsController(
+ ITokenStandardRegistry registry,
+ ITokenStandardValidator validator,
+ ILogger logger)
+ {
+ _registry = registry;
+ _validator = validator;
+ _logger = logger;
+ }
+
+ ///
+ /// Gets all supported token standards and their requirements
+ ///
+ /// Optional filters for standard retrieval
+ /// List of supported token standard profiles
+ ///
+ /// This endpoint returns a comprehensive list of all supported token standards,
+ /// including their version, required fields, optional fields, and validation rules.
+ /// Use this endpoint to discover what standards are available and what fields are
+ /// required for each standard when creating tokens.
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(GetTokenStandardsResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task GetStandards([FromQuery] GetTokenStandardsRequest? request)
+ {
+ try
+ {
+ var correlationId = Guid.NewGuid().ToString();
+ _logger.LogInformation(
+ "Standards discovery request received. CorrelationId={CorrelationId}",
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ var activeOnly = request?.ActiveOnly ?? true;
+ var standards = await _registry.GetAllStandardsAsync(activeOnly);
+
+ if (request?.Standard.HasValue == true)
+ {
+ standards = standards.Where(s => s.Standard == request.Standard.Value).ToList();
+ }
+
+ var response = new GetTokenStandardsResponse
+ {
+ Standards = standards,
+ TotalCount = standards.Count
+ };
+
+ _logger.LogInformation(
+ "Standards discovery completed successfully. Count={Count}, CorrelationId={CorrelationId}",
+ standards.Count,
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ return Ok(response);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error retrieving token standards");
+ return StatusCode(StatusCodes.Status500InternalServerError, new ApiErrorResponse
+ {
+ ErrorCode = ErrorCodes.INTERNAL_SERVER_ERROR,
+ ErrorMessage = "An error occurred while retrieving token standards"
+ });
+ }
+ }
+
+ ///
+ /// Gets details for a specific token standard
+ ///
+ /// The token standard to retrieve
+ /// Token standard profile details
+ ///
+ /// Returns detailed information about a specific token standard, including all
+ /// required and optional fields, validation rules, and example metadata.
+ ///
+ [HttpGet("{standard}")]
+ [ProducesResponseType(typeof(TokenStandardProfile), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task GetStandard([FromRoute] TokenStandard standard)
+ {
+ try
+ {
+ var correlationId = Guid.NewGuid().ToString();
+ _logger.LogInformation(
+ "Standard profile request for {Standard}. CorrelationId={CorrelationId}",
+ LoggingHelper.SanitizeLogInput(standard.ToString()),
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ var profile = await _registry.GetStandardProfileAsync(standard);
+ if (profile == null)
+ {
+ return NotFound(new ApiErrorResponse
+ {
+ ErrorCode = ErrorCodes.NOT_FOUND,
+ ErrorMessage = $"Token standard '{standard}' not found or not supported"
+ });
+ }
+
+ return Ok(profile);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error retrieving token standard profile for {Standard}",
+ LoggingHelper.SanitizeLogInput(standard.ToString()));
+ return StatusCode(StatusCodes.Status500InternalServerError, new ApiErrorResponse
+ {
+ ErrorCode = ErrorCodes.INTERNAL_SERVER_ERROR,
+ ErrorMessage = "An error occurred while retrieving token standard profile"
+ });
+ }
+ }
+
+ ///
+ /// Validates token metadata against a specified standard (preflight validation)
+ ///
+ /// Validation request with metadata and standard
+ /// Validation result with any errors or warnings
+ ///
+ /// This endpoint performs preflight validation of token metadata against a specified
+ /// standard without creating a token. Use this to validate metadata before submitting
+ /// a full token creation request. The response includes detailed error messages and
+ /// field-specific validation feedback.
+ ///
+ /// **Performance:** Target p95 latency is under 200ms for typical payloads.
+ ///
+ [HttpPost("validate")]
+ [ProducesResponseType(typeof(ValidateTokenMetadataResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task ValidateMetadata([FromBody] ValidateTokenMetadataRequest request)
+ {
+ var correlationId = Guid.NewGuid().ToString();
+ var startTime = DateTime.UtcNow;
+
+ try
+ {
+ _logger.LogInformation(
+ "Metadata validation request for standard {Standard}. CorrelationId={CorrelationId}",
+ LoggingHelper.SanitizeLogInput(request.Standard.ToString()),
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ // Check if standard is supported
+ var isSupported = await _registry.IsStandardSupportedAsync(request.Standard);
+ if (!isSupported)
+ {
+ return BadRequest(new ValidateTokenMetadataResponse
+ {
+ IsValid = false,
+ ErrorCode = ErrorCodes.INVALID_TOKEN_STANDARD,
+ Message = $"Token standard '{request.Standard}' is not supported",
+ CorrelationId = correlationId
+ });
+ }
+
+ // Perform validation
+ var validationResult = await _validator.ValidateAsync(
+ request.Standard,
+ request.Metadata,
+ request.Name,
+ request.Symbol,
+ request.Decimals);
+
+ var response = new ValidateTokenMetadataResponse
+ {
+ IsValid = validationResult.IsValid,
+ ValidationResult = validationResult,
+ Message = validationResult.Message,
+ CorrelationId = correlationId
+ };
+
+ if (!validationResult.IsValid && validationResult.Errors.Count > 0)
+ {
+ response.ErrorCode = ErrorCodes.METADATA_VALIDATION_FAILED;
+ }
+
+ var duration = (DateTime.UtcNow - startTime).TotalMilliseconds;
+ _logger.LogInformation(
+ "Metadata validation completed. Standard={Standard}, IsValid={IsValid}, Errors={ErrorCount}, Warnings={WarningCount}, Duration={Duration}ms, CorrelationId={CorrelationId}",
+ LoggingHelper.SanitizeLogInput(request.Standard.ToString()),
+ validationResult.IsValid,
+ validationResult.Errors.Count,
+ validationResult.Warnings.Count,
+ duration,
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ return Ok(response);
+ }
+ catch (Exception ex)
+ {
+ var duration = (DateTime.UtcNow - startTime).TotalMilliseconds;
+ _logger.LogError(ex,
+ "Error validating token metadata. Standard={Standard}, Duration={Duration}ms, CorrelationId={CorrelationId}",
+ LoggingHelper.SanitizeLogInput(request.Standard.ToString()),
+ duration,
+ LoggingHelper.SanitizeLogInput(correlationId));
+
+ return StatusCode(StatusCodes.Status500InternalServerError, new ValidateTokenMetadataResponse
+ {
+ IsValid = false,
+ ErrorCode = ErrorCodes.INTERNAL_SERVER_ERROR,
+ Message = "An error occurred during metadata validation",
+ CorrelationId = correlationId
+ });
+ }
+ }
+ }
+}
diff --git a/BiatecTokensApi/Models/ErrorCodes.cs b/BiatecTokensApi/Models/ErrorCodes.cs
index ddb97d1..8ee7d56 100644
--- a/BiatecTokensApi/Models/ErrorCodes.cs
+++ b/BiatecTokensApi/Models/ErrorCodes.cs
@@ -162,5 +162,36 @@ public static class ErrorCodes
/// Recovery cooldown active
///
public const string RECOVERY_COOLDOWN_ACTIVE = "RECOVERY_COOLDOWN_ACTIVE";
+
+ // Token Standard Validation errors
+ ///
+ /// Token metadata validation failed
+ ///
+ public const string METADATA_VALIDATION_FAILED = "METADATA_VALIDATION_FAILED";
+
+ ///
+ /// Invalid token standard specified
+ ///
+ public const string INVALID_TOKEN_STANDARD = "INVALID_TOKEN_STANDARD";
+
+ ///
+ /// Required metadata field missing
+ ///
+ public const string REQUIRED_METADATA_FIELD_MISSING = "REQUIRED_METADATA_FIELD_MISSING";
+
+ ///
+ /// Metadata field type mismatch
+ ///
+ public const string METADATA_FIELD_TYPE_MISMATCH = "METADATA_FIELD_TYPE_MISMATCH";
+
+ ///
+ /// Metadata field validation failed
+ ///
+ public const string METADATA_FIELD_VALIDATION_FAILED = "METADATA_FIELD_VALIDATION_FAILED";
+
+ ///
+ /// Token standard not supported
+ ///
+ public const string TOKEN_STANDARD_NOT_SUPPORTED = "TOKEN_STANDARD_NOT_SUPPORTED";
}
}
diff --git a/BiatecTokensApi/Models/TokenIssuanceAuditLog.cs b/BiatecTokensApi/Models/TokenIssuanceAuditLog.cs
index 2bf928a..f8c2c80 100644
--- a/BiatecTokensApi/Models/TokenIssuanceAuditLog.cs
+++ b/BiatecTokensApi/Models/TokenIssuanceAuditLog.cs
@@ -143,6 +143,41 @@ public class TokenIssuanceAuditLogEntry
/// Correlation ID for related events
///
public string? CorrelationId { get; set; }
+
+ ///
+ /// Token standard profile used for validation (e.g., "ARC3", "ERC20", "Baseline")
+ ///
+ public string? TokenStandard { get; set; }
+
+ ///
+ /// Version of the token standard profile used
+ ///
+ public string? StandardVersion { get; set; }
+
+ ///
+ /// Whether metadata validation was performed
+ ///
+ public bool ValidationPerformed { get; set; }
+
+ ///
+ /// Result of metadata validation
+ ///
+ public string? ValidationStatus { get; set; }
+
+ ///
+ /// Validation error messages if validation failed
+ ///
+ public string? ValidationErrors { get; set; }
+
+ ///
+ /// Validation warning messages
+ ///
+ public string? ValidationWarnings { get; set; }
+
+ ///
+ /// Timestamp when validation was performed
+ ///
+ public DateTime? ValidationTimestamp { get; set; }
}
///
diff --git a/BiatecTokensApi/Models/TokenStandards/StandardsApiModels.cs b/BiatecTokensApi/Models/TokenStandards/StandardsApiModels.cs
new file mode 100644
index 0000000..87de6ee
--- /dev/null
+++ b/BiatecTokensApi/Models/TokenStandards/StandardsApiModels.cs
@@ -0,0 +1,106 @@
+namespace BiatecTokensApi.Models.TokenStandards
+{
+ ///
+ /// Request for standards discovery endpoint
+ ///
+ public class GetTokenStandardsRequest
+ {
+ ///
+ /// Optional filter to only return active standards
+ ///
+ public bool? ActiveOnly { get; set; }
+
+ ///
+ /// Optional filter by specific standard
+ ///
+ public TokenStandard? Standard { get; set; }
+ }
+
+ ///
+ /// Response containing list of supported token standards
+ ///
+ public class GetTokenStandardsResponse
+ {
+ ///
+ /// List of available token standard profiles
+ ///
+ public List Standards { get; set; } = new();
+
+ ///
+ /// Total number of standards available
+ ///
+ public int TotalCount { get; set; }
+ }
+
+ ///
+ /// Request to validate token metadata against a standard
+ ///
+ public class ValidateTokenMetadataRequest
+ {
+ ///
+ /// Token standard to validate against
+ ///
+ public TokenStandard Standard { get; set; } = TokenStandard.Baseline;
+
+ ///
+ /// Token metadata as JSON object
+ ///
+ public object? Metadata { get; set; }
+
+ ///
+ /// Token type (optional, for context)
+ ///
+ public string? TokenType { get; set; }
+
+ ///
+ /// Token name
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Token symbol
+ ///
+ public string? Symbol { get; set; }
+
+ ///
+ /// Number of decimals
+ ///
+ public int? Decimals { get; set; }
+
+ ///
+ /// Total supply
+ ///
+ public string? TotalSupply { get; set; }
+ }
+
+ ///
+ /// Response from metadata validation
+ ///
+ public class ValidateTokenMetadataResponse
+ {
+ ///
+ /// Whether validation passed
+ ///
+ public bool IsValid { get; set; }
+
+ ///
+ /// Validation result details
+ ///
+ public TokenValidationResult? ValidationResult { get; set; }
+
+ ///
+ /// Error code if validation failed
+ ///
+ public string? ErrorCode { get; set; }
+
+ ///
+ /// Human-readable message
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Correlation ID for tracking
+ ///
+ public string? CorrelationId { get; set; }
+ }
+}
diff --git a/BiatecTokensApi/Models/TokenStandards/TokenStandard.cs b/BiatecTokensApi/Models/TokenStandards/TokenStandard.cs
new file mode 100644
index 0000000..c270b2b
--- /dev/null
+++ b/BiatecTokensApi/Models/TokenStandards/TokenStandard.cs
@@ -0,0 +1,202 @@
+namespace BiatecTokensApi.Models.TokenStandards
+{
+ ///
+ /// Enumeration of supported token standards for validation and compliance
+ ///
+ public enum TokenStandard
+ {
+ ///
+ /// Baseline standard with minimal validation requirements
+ ///
+ Baseline,
+
+ ///
+ /// ARC-3 standard for Algorand tokens with rich metadata
+ ///
+ ARC3,
+
+ ///
+ /// ARC-19 standard for Algorand tokens with on-chain metadata
+ ///
+ ARC19,
+
+ ///
+ /// ARC-69 standard for Algorand tokens with simplified metadata
+ ///
+ ARC69,
+
+ ///
+ /// ERC-20 standard for Ethereum-compatible tokens
+ ///
+ ERC20
+ }
+
+ ///
+ /// Token standard profile defining validation rules and requirements
+ ///
+ public class TokenStandardProfile
+ {
+ ///
+ /// Unique identifier for the standard profile
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Display name of the standard
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Version of the standard profile
+ ///
+ public string Version { get; set; } = "1.0.0";
+
+ ///
+ /// Human-readable description of the standard
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Token standard enumeration value
+ ///
+ public TokenStandard Standard { get; set; }
+
+ ///
+ /// List of required metadata fields for this standard
+ ///
+ public List RequiredFields { get; set; } = new();
+
+ ///
+ /// List of optional metadata fields for this standard
+ ///
+ public List OptionalFields { get; set; } = new();
+
+ ///
+ /// Validation rules for this standard
+ ///
+ public List ValidationRules { get; set; } = new();
+
+ ///
+ /// Whether this profile is currently active and available for use
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// Example metadata JSON for this standard
+ ///
+ public string? ExampleJson { get; set; }
+
+ ///
+ /// External reference URL for standard specification
+ ///
+ public string? SpecificationUrl { get; set; }
+ }
+
+ ///
+ /// Definition of a metadata field for a token standard
+ ///
+ public class StandardFieldDefinition
+ {
+ ///
+ /// Field name
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Expected data type (string, number, boolean, object, array)
+ ///
+ public string DataType { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable description of the field
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Default value if not provided (for optional fields)
+ ///
+ public object? DefaultValue { get; set; }
+
+ ///
+ /// Regular expression pattern for validation (for string types)
+ ///
+ public string? ValidationPattern { get; set; }
+
+ ///
+ /// Minimum value (for numeric types)
+ ///
+ public double? MinValue { get; set; }
+
+ ///
+ /// Maximum value (for numeric types)
+ ///
+ public double? MaxValue { get; set; }
+
+ ///
+ /// Maximum length (for string types)
+ ///
+ public int? MaxLength { get; set; }
+
+ ///
+ /// Whether this field is required
+ ///
+ public bool IsRequired { get; set; }
+ }
+
+ ///
+ /// Validation rule for token standard compliance
+ ///
+ public class ValidationRule
+ {
+ ///
+ /// Unique identifier for the rule
+ ///
+ public string Id { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable name of the rule
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Description of what this rule validates
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Error message to display when validation fails
+ ///
+ public string ErrorMessage { get; set; } = string.Empty;
+
+ ///
+ /// Error code for programmatic handling
+ ///
+ public string ErrorCode { get; set; } = string.Empty;
+
+ ///
+ /// Severity level of the validation rule
+ ///
+ public TokenValidationSeverity Severity { get; set; } = TokenValidationSeverity.Error;
+ }
+
+ ///
+ /// Severity levels for token validation rules
+ ///
+ public enum TokenValidationSeverity
+ {
+ ///
+ /// Informational message, does not prevent token creation
+ ///
+ Info,
+
+ ///
+ /// Warning message, may indicate potential issues
+ ///
+ Warning,
+
+ ///
+ /// Error that must be fixed before token creation
+ ///
+ Error
+ }
+}
diff --git a/BiatecTokensApi/Models/TokenStandards/ValidationResult.cs b/BiatecTokensApi/Models/TokenStandards/ValidationResult.cs
new file mode 100644
index 0000000..92aaa31
--- /dev/null
+++ b/BiatecTokensApi/Models/TokenStandards/ValidationResult.cs
@@ -0,0 +1,141 @@
+namespace BiatecTokensApi.Models.TokenStandards
+{
+ ///
+ /// Result of token metadata validation against a standard profile
+ ///
+ public class TokenValidationResult
+ {
+ ///
+ /// Whether the validation passed
+ ///
+ public bool IsValid { get; set; }
+
+ ///
+ /// Token standard that was validated against
+ ///
+ public TokenStandard Standard { get; set; }
+
+ ///
+ /// Version of the standard profile used for validation
+ ///
+ public string StandardVersion { get; set; } = string.Empty;
+
+ ///
+ /// List of validation errors encountered
+ ///
+ public List Errors { get; set; } = new();
+
+ ///
+ /// List of validation warnings
+ ///
+ public List Warnings { get; set; } = new();
+
+ ///
+ /// Timestamp when validation was performed
+ ///
+ public DateTime ValidatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Summary message describing validation status
+ ///
+ public string? Message { get; set; }
+ }
+
+ ///
+ /// Individual validation error or warning
+ ///
+ public class ValidationError
+ {
+ ///
+ /// Error code for programmatic handling
+ ///
+ public string Code { get; set; } = string.Empty;
+
+ ///
+ /// Field name that failed validation
+ ///
+ public string Field { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable error message
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// Severity level
+ ///
+ public TokenValidationSeverity Severity { get; set; } = TokenValidationSeverity.Error;
+
+ ///
+ /// Additional context about the error
+ ///
+ public string? Details { get; set; }
+ }
+
+ ///
+ /// Token metadata with validation status
+ ///
+ public class ValidatedTokenMetadata
+ {
+ ///
+ /// Selected token standard profile
+ ///
+ public TokenStandard Standard { get; set; } = TokenStandard.Baseline;
+
+ ///
+ /// Version of the standard profile
+ ///
+ public string StandardVersion { get; set; } = "1.0.0";
+
+ ///
+ /// Current validation status
+ ///
+ public ValidationStatus Status { get; set; } = ValidationStatus.NotValidated;
+
+ ///
+ /// Last validation timestamp
+ ///
+ public DateTime? LastValidatedAt { get; set; }
+
+ ///
+ /// Validation result message
+ ///
+ public string? ValidationMessage { get; set; }
+
+ ///
+ /// Correlation ID for tracking validation through logs
+ ///
+ public string? CorrelationId { get; set; }
+ }
+
+ ///
+ /// Validation status enumeration
+ ///
+ public enum ValidationStatus
+ {
+ ///
+ /// Not yet validated
+ ///
+ NotValidated,
+
+ ///
+ /// Validation in progress
+ ///
+ Validating,
+
+ ///
+ /// Validation passed successfully
+ ///
+ Valid,
+
+ ///
+ /// Validation failed with errors
+ ///
+ Invalid,
+
+ ///
+ /// Validation passed with warnings
+ ///
+ ValidWithWarnings
+ }
+}
diff --git a/BiatecTokensApi/Program.cs b/BiatecTokensApi/Program.cs
index ef05afb..9283fc3 100644
--- a/BiatecTokensApi/Program.cs
+++ b/BiatecTokensApi/Program.cs
@@ -149,6 +149,8 @@ public static void Main(string[] args)
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
var authOptions = builder.Configuration.GetSection("AlgorandAuthentication").Get();
if (authOptions == null) throw new Exception("Config for the authentication is missing");
diff --git a/BiatecTokensApi/Services/Interface/ITokenStandardRegistry.cs b/BiatecTokensApi/Services/Interface/ITokenStandardRegistry.cs
new file mode 100644
index 0000000..7939c3b
--- /dev/null
+++ b/BiatecTokensApi/Services/Interface/ITokenStandardRegistry.cs
@@ -0,0 +1,37 @@
+using BiatecTokensApi.Models.TokenStandards;
+
+namespace BiatecTokensApi.Services.Interface
+{
+ ///
+ /// Service for managing and retrieving token standard profiles
+ ///
+ public interface ITokenStandardRegistry
+ {
+ ///
+ /// Gets all available token standard profiles
+ ///
+ /// If true, only returns active profiles
+ /// List of token standard profiles
+ Task> GetAllStandardsAsync(bool activeOnly = true);
+
+ ///
+ /// Gets a specific token standard profile by standard type
+ ///
+ /// The token standard to retrieve
+ /// Token standard profile or null if not found
+ Task GetStandardProfileAsync(TokenStandard standard);
+
+ ///
+ /// Gets the default token standard for backward compatibility
+ ///
+ /// Default token standard profile
+ Task GetDefaultStandardAsync();
+
+ ///
+ /// Checks if a token standard is supported
+ ///
+ /// The token standard to check
+ /// True if supported, false otherwise
+ Task IsStandardSupportedAsync(TokenStandard standard);
+ }
+}
diff --git a/BiatecTokensApi/Services/Interface/ITokenStandardValidator.cs b/BiatecTokensApi/Services/Interface/ITokenStandardValidator.cs
new file mode 100644
index 0000000..d8af6ef
--- /dev/null
+++ b/BiatecTokensApi/Services/Interface/ITokenStandardValidator.cs
@@ -0,0 +1,53 @@
+using BiatecTokensApi.Models.TokenStandards;
+
+namespace BiatecTokensApi.Services.Interface
+{
+ ///
+ /// Service for validating token metadata against standards
+ ///
+ public interface ITokenStandardValidator
+ {
+ ///
+ /// Validates token metadata against a specified standard profile
+ ///
+ /// The token standard to validate against
+ /// The metadata object to validate
+ /// Optional token name for context
+ /// Optional token symbol for context
+ /// Optional decimals for context
+ /// Validation result with errors and warnings
+ Task ValidateAsync(
+ TokenStandard standard,
+ object? metadata,
+ string? tokenName = null,
+ string? tokenSymbol = null,
+ int? decimals = null);
+
+ ///
+ /// Validates that required fields are present
+ ///
+ /// The standard profile to validate against
+ /// The metadata object to validate
+ /// List of validation errors for missing required fields
+ Task> ValidateRequiredFieldsAsync(
+ TokenStandardProfile profile,
+ object? metadata);
+
+ ///
+ /// Validates field types and constraints
+ ///
+ /// The standard profile to validate against
+ /// The metadata object to validate
+ /// List of validation errors for field type mismatches
+ Task> ValidateFieldTypesAsync(
+ TokenStandardProfile profile,
+ object? metadata);
+
+ ///
+ /// Checks if the validator supports a given standard
+ ///
+ /// The token standard to check
+ /// True if supported, false otherwise
+ bool SupportsStandard(TokenStandard standard);
+ }
+}
diff --git a/BiatecTokensApi/Services/TokenStandardRegistry.cs b/BiatecTokensApi/Services/TokenStandardRegistry.cs
new file mode 100644
index 0000000..1e8b9d3
--- /dev/null
+++ b/BiatecTokensApi/Services/TokenStandardRegistry.cs
@@ -0,0 +1,489 @@
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services.Interface;
+
+namespace BiatecTokensApi.Services
+{
+ ///
+ /// Registry service for managing token standard profiles
+ ///
+ public class TokenStandardRegistry : ITokenStandardRegistry
+ {
+ private readonly ILogger _logger;
+ private readonly List _profiles;
+
+ ///
+ /// Initializes a new instance of the TokenStandardRegistry
+ ///
+ /// Logger instance
+ public TokenStandardRegistry(ILogger logger)
+ {
+ _logger = logger;
+ _profiles = InitializeStandardProfiles();
+ }
+
+ ///
+ /// Gets all available token standard profiles
+ ///
+ public Task> GetAllStandardsAsync(bool activeOnly = true)
+ {
+ var standards = activeOnly
+ ? _profiles.Where(p => p.IsActive).ToList()
+ : _profiles.ToList();
+
+ return Task.FromResult(standards);
+ }
+
+ ///
+ /// Gets a specific token standard profile by standard type
+ ///
+ public Task GetStandardProfileAsync(TokenStandard standard)
+ {
+ var profile = _profiles.FirstOrDefault(p => p.Standard == standard && p.IsActive);
+ return Task.FromResult(profile);
+ }
+
+ ///
+ /// Gets the default token standard for backward compatibility
+ ///
+ public Task GetDefaultStandardAsync()
+ {
+ var defaultProfile = _profiles.First(p => p.Standard == TokenStandard.Baseline);
+ return Task.FromResult(defaultProfile);
+ }
+
+ ///
+ /// Checks if a token standard is supported
+ ///
+ public Task IsStandardSupportedAsync(TokenStandard standard)
+ {
+ var isSupported = _profiles.Any(p => p.Standard == standard && p.IsActive);
+ return Task.FromResult(isSupported);
+ }
+
+ ///
+ /// Initializes all supported token standard profiles
+ ///
+ private List InitializeStandardProfiles()
+ {
+ return new List
+ {
+ CreateBaselineProfile(),
+ CreateARC3Profile(),
+ CreateARC19Profile(),
+ CreateARC69Profile(),
+ CreateERC20Profile()
+ };
+ }
+
+ ///
+ /// Creates the Baseline standard profile
+ ///
+ private TokenStandardProfile CreateBaselineProfile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "baseline-1.0",
+ Name = "Baseline",
+ Version = "1.0.0",
+ Description = "Minimal validation requirements for backward compatibility",
+ Standard = TokenStandard.Baseline,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ Description = "Token name",
+ IsRequired = true,
+ MaxLength = 256
+ }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "decimals",
+ DataType = "number",
+ Description = "Number of decimal places",
+ IsRequired = false,
+ MinValue = 0,
+ MaxValue = 18
+ },
+ new StandardFieldDefinition
+ {
+ Name = "description",
+ DataType = "string",
+ Description = "Token description",
+ IsRequired = false,
+ MaxLength = 1000
+ }
+ },
+ ValidationRules = new List(),
+ IsActive = true,
+ SpecificationUrl = null
+ };
+ }
+
+ ///
+ /// Creates the ARC-3 standard profile
+ ///
+ private TokenStandardProfile CreateARC3Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc3-1.0",
+ Name = "ARC-3",
+ Version = "1.0.0",
+ Description = "Algorand Request for Comments 3 - Rich metadata standard for NFTs and tokens",
+ Standard = TokenStandard.ARC3,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ Description = "Identifies the asset to which this token represents",
+ IsRequired = true,
+ MaxLength = 256
+ }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "decimals",
+ DataType = "number",
+ Description = "The number of decimal places that the token amount should display",
+ IsRequired = false,
+ MinValue = 0,
+ MaxValue = 19
+ },
+ new StandardFieldDefinition
+ {
+ Name = "description",
+ DataType = "string",
+ Description = "Describes the asset to which this token represents",
+ IsRequired = false,
+ MaxLength = 1000
+ },
+ new StandardFieldDefinition
+ {
+ Name = "image",
+ DataType = "string",
+ Description = "A URI pointing to a file with MIME type image/*",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "image_integrity",
+ DataType = "string",
+ Description = "The SHA-256 digest of the file pointed by the URI image",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "image_mimetype",
+ DataType = "string",
+ Description = "The MIME type of the file pointed by the URI image",
+ IsRequired = false,
+ ValidationPattern = "^image/.*"
+ },
+ new StandardFieldDefinition
+ {
+ Name = "background_color",
+ DataType = "string",
+ Description = "Background color (six-character hexadecimal without #)",
+ IsRequired = false,
+ ValidationPattern = "^[0-9A-Fa-f]{6}$"
+ },
+ new StandardFieldDefinition
+ {
+ Name = "external_url",
+ DataType = "string",
+ Description = "A URI pointing to an external website presenting the asset",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "animation_url",
+ DataType = "string",
+ Description = "A URI pointing to a multi-media file representing the asset",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "properties",
+ DataType = "object",
+ Description = "Arbitrary properties (attributes)",
+ IsRequired = false
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc3-image-mimetype",
+ Name = "Image MIME type validation",
+ Description = "If image is provided, image_mimetype should start with 'image/'",
+ ErrorMessage = "Image MIME type must start with 'image/'",
+ ErrorCode = "ARC3_INVALID_IMAGE_MIMETYPE",
+ Severity = TokenValidationSeverity.Warning
+ },
+ new ValidationRule
+ {
+ Id = "arc3-background-color",
+ Name = "Background color format",
+ Description = "Background color must be a six-character hexadecimal without #",
+ ErrorMessage = "Background color must be in format RRGGBB (e.g., 'FF0000' for red)",
+ ErrorCode = "ARC3_INVALID_BACKGROUND_COLOR",
+ Severity = TokenValidationSeverity.Error
+ }
+ },
+ IsActive = true,
+ SpecificationUrl = "https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0003.md",
+ ExampleJson = @"{
+ ""name"": ""My Token"",
+ ""decimals"": 6,
+ ""description"": ""A sample ARC-3 token"",
+ ""image"": ""ipfs://QmXyz..."",
+ ""image_integrity"": ""sha256-abc123..."",
+ ""image_mimetype"": ""image/png"",
+ ""properties"": {
+ ""category"": ""utility"",
+ ""supply"": 1000000
+ }
+}"
+ };
+ }
+
+ ///
+ /// Creates the ARC-19 standard profile
+ ///
+ private TokenStandardProfile CreateARC19Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc19-1.0",
+ Name = "ARC-19",
+ Version = "1.0.0",
+ Description = "Algorand Request for Comments 19 - On-chain metadata standard",
+ Standard = TokenStandard.ARC19,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ Description = "Asset name stored on-chain",
+ IsRequired = true,
+ MaxLength = 32
+ },
+ new StandardFieldDefinition
+ {
+ Name = "unit_name",
+ DataType = "string",
+ Description = "Asset unit name",
+ IsRequired = true,
+ MaxLength = 8
+ }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "url",
+ DataType = "string",
+ Description = "Asset URL pointing to metadata",
+ IsRequired = false,
+ MaxLength = 96
+ },
+ new StandardFieldDefinition
+ {
+ Name = "decimals",
+ DataType = "number",
+ Description = "Number of decimals",
+ IsRequired = false,
+ MinValue = 0,
+ MaxValue = 19
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc19-name-length",
+ Name = "Name length constraint",
+ Description = "Asset name must not exceed 32 characters for on-chain storage",
+ ErrorMessage = "Asset name must be 32 characters or less",
+ ErrorCode = "ARC19_NAME_TOO_LONG",
+ Severity = TokenValidationSeverity.Error
+ },
+ new ValidationRule
+ {
+ Id = "arc19-unit-name-length",
+ Name = "Unit name length constraint",
+ Description = "Unit name must not exceed 8 characters",
+ ErrorMessage = "Unit name must be 8 characters or less",
+ ErrorCode = "ARC19_UNIT_NAME_TOO_LONG",
+ Severity = TokenValidationSeverity.Error
+ }
+ },
+ IsActive = true,
+ SpecificationUrl = "https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0019.md"
+ };
+ }
+
+ ///
+ /// Creates the ARC-69 standard profile
+ ///
+ private TokenStandardProfile CreateARC69Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc69-1.0",
+ Name = "ARC-69",
+ Version = "1.0.0",
+ Description = "Algorand Request for Comments 69 - Simplified metadata standard",
+ Standard = TokenStandard.ARC69,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "standard",
+ DataType = "string",
+ Description = "Must be 'arc69'",
+ IsRequired = true
+ }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "description",
+ DataType = "string",
+ Description = "Description of the asset",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "external_url",
+ DataType = "string",
+ Description = "URL to external website",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "media_url",
+ DataType = "string",
+ Description = "URL to media file",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "properties",
+ DataType = "object",
+ Description = "Arbitrary properties",
+ IsRequired = false
+ },
+ new StandardFieldDefinition
+ {
+ Name = "mime_type",
+ DataType = "string",
+ Description = "MIME type of the media",
+ IsRequired = false
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc69-standard-field",
+ Name = "Standard field value",
+ Description = "The 'standard' field must be set to 'arc69'",
+ ErrorMessage = "The 'standard' field must equal 'arc69'",
+ ErrorCode = "ARC69_INVALID_STANDARD_FIELD",
+ Severity = TokenValidationSeverity.Error
+ }
+ },
+ IsActive = true,
+ SpecificationUrl = "https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0069.md"
+ };
+ }
+
+ ///
+ /// Creates the ERC-20 standard profile
+ ///
+ private TokenStandardProfile CreateERC20Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "erc20-1.0",
+ Name = "ERC-20",
+ Version = "1.0.0",
+ Description = "Ethereum Request for Comments 20 - Fungible token standard",
+ Standard = TokenStandard.ERC20,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ Description = "Token name",
+ IsRequired = true,
+ MaxLength = 256
+ },
+ new StandardFieldDefinition
+ {
+ Name = "symbol",
+ DataType = "string",
+ Description = "Token symbol",
+ IsRequired = true,
+ MaxLength = 11
+ },
+ new StandardFieldDefinition
+ {
+ Name = "decimals",
+ DataType = "number",
+ Description = "Number of decimal places",
+ IsRequired = true,
+ MinValue = 0,
+ MaxValue = 18
+ }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "totalSupply",
+ DataType = "string",
+ Description = "Total token supply",
+ IsRequired = false
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "erc20-symbol-length",
+ Name = "Symbol length constraint",
+ Description = "Token symbol should be 11 characters or less",
+ ErrorMessage = "Token symbol must be 11 characters or less",
+ ErrorCode = "ERC20_SYMBOL_TOO_LONG",
+ Severity = TokenValidationSeverity.Error
+ },
+ new ValidationRule
+ {
+ Id = "erc20-decimals-range",
+ Name = "Decimals range validation",
+ Description = "Decimals must be between 0 and 18",
+ ErrorMessage = "Decimals must be between 0 and 18",
+ ErrorCode = "ERC20_INVALID_DECIMALS",
+ Severity = TokenValidationSeverity.Error
+ }
+ },
+ IsActive = true,
+ SpecificationUrl = "https://eips.ethereum.org/EIPS/eip-20"
+ };
+ }
+ }
+}
diff --git a/BiatecTokensApi/Services/TokenStandardValidator.cs b/BiatecTokensApi/Services/TokenStandardValidator.cs
new file mode 100644
index 0000000..8102539
--- /dev/null
+++ b/BiatecTokensApi/Services/TokenStandardValidator.cs
@@ -0,0 +1,552 @@
+using BiatecTokensApi.Helpers;
+using BiatecTokensApi.Models;
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services.Interface;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace BiatecTokensApi.Services
+{
+ ///
+ /// Service for validating token metadata against standards
+ ///
+ public class TokenStandardValidator : ITokenStandardValidator
+ {
+ private readonly ILogger _logger;
+ private readonly ITokenStandardRegistry _registry;
+
+ ///
+ /// Initializes a new instance of the TokenStandardValidator
+ ///
+ /// Logger instance
+ /// Token standard registry
+ public TokenStandardValidator(
+ ILogger logger,
+ ITokenStandardRegistry registry)
+ {
+ _logger = logger;
+ _registry = registry;
+ }
+
+ ///
+ /// Validates token metadata against a specified standard profile
+ ///
+ public async Task ValidateAsync(
+ TokenStandard standard,
+ object? metadata,
+ string? tokenName = null,
+ string? tokenSymbol = null,
+ int? decimals = null)
+ {
+ var result = new TokenValidationResult
+ {
+ Standard = standard,
+ ValidatedAt = DateTime.UtcNow
+ };
+
+ try
+ {
+ // Get the standard profile
+ var profile = await _registry.GetStandardProfileAsync(standard);
+ if (profile == null)
+ {
+ result.IsValid = false;
+ result.Errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.INVALID_TOKEN_STANDARD,
+ Field = "standard",
+ Message = $"Token standard '{standard}' is not supported",
+ Severity = TokenValidationSeverity.Error
+ });
+ return result;
+ }
+
+ result.StandardVersion = profile.Version;
+
+ // Convert metadata to dictionary for easier validation
+ var metadataDict = ConvertToDictionary(metadata);
+
+ // Add context fields if provided
+ if (!string.IsNullOrEmpty(tokenName))
+ {
+ metadataDict["name"] = tokenName;
+ }
+ if (!string.IsNullOrEmpty(tokenSymbol))
+ {
+ metadataDict["symbol"] = tokenSymbol;
+ }
+ if (decimals.HasValue)
+ {
+ metadataDict["decimals"] = decimals.Value;
+ }
+
+ // Validate required fields
+ var requiredFieldErrors = await ValidateRequiredFieldsInternal(profile, metadataDict);
+ result.Errors.AddRange(requiredFieldErrors);
+
+ // Validate field types and constraints
+ var fieldTypeErrors = await ValidateFieldTypesInternal(profile, metadataDict);
+ result.Errors.AddRange(fieldTypeErrors);
+
+ // Apply custom validation rules
+ var ruleErrors = await ValidateCustomRulesAsync(profile, metadataDict);
+ result.Errors.AddRange(ruleErrors.Where(e => e.Severity == TokenValidationSeverity.Error));
+ result.Warnings.AddRange(ruleErrors.Where(e => e.Severity == TokenValidationSeverity.Warning));
+
+ // Set overall validity
+ result.IsValid = result.Errors.Count == 0;
+ result.Message = result.IsValid
+ ? (result.Warnings.Count > 0
+ ? $"Validation passed with {result.Warnings.Count} warning(s)"
+ : "Validation passed successfully")
+ : $"Validation failed with {result.Errors.Count} error(s)";
+
+ _logger.LogInformation(
+ "Token metadata validation completed for standard {Standard}: IsValid={IsValid}, Errors={ErrorCount}, Warnings={WarningCount}",
+ LoggingHelper.SanitizeLogInput(standard.ToString()),
+ result.IsValid,
+ result.Errors.Count,
+ result.Warnings.Count);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error validating token metadata for standard {Standard}",
+ LoggingHelper.SanitizeLogInput(standard.ToString()));
+ result.IsValid = false;
+ result.Errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.UNEXPECTED_ERROR,
+ Field = "metadata",
+ Message = "An unexpected error occurred during validation",
+ Severity = TokenValidationSeverity.Error
+ });
+ }
+
+ return result;
+ }
+
+ ///
+ /// Validates that required fields are present
+ ///
+ public async Task> ValidateRequiredFieldsAsync(
+ TokenStandardProfile profile,
+ object? metadata)
+ {
+ var metadataDict = ConvertToDictionary(metadata);
+ return await ValidateRequiredFieldsInternal(profile, metadataDict);
+ }
+
+ ///
+ /// Validates field types and constraints
+ ///
+ public async Task> ValidateFieldTypesAsync(
+ TokenStandardProfile profile,
+ object? metadata)
+ {
+ var metadataDict = ConvertToDictionary(metadata);
+ return await ValidateFieldTypesInternal(profile, metadataDict);
+ }
+
+ ///
+ /// Checks if the validator supports a given standard
+ ///
+ public bool SupportsStandard(TokenStandard standard)
+ {
+ return true; // This validator supports all standards through the registry
+ }
+
+ ///
+ /// Internal method to validate required fields
+ ///
+ private Task> ValidateRequiredFieldsInternal(
+ TokenStandardProfile profile,
+ Dictionary metadataDict)
+ {
+ var errors = new List();
+
+ foreach (var field in profile.RequiredFields)
+ {
+ if (!metadataDict.ContainsKey(field.Name) || metadataDict[field.Name] == null)
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.REQUIRED_METADATA_FIELD_MISSING,
+ Field = field.Name,
+ Message = $"Required field '{field.Name}' is missing",
+ Severity = TokenValidationSeverity.Error,
+ Details = field.Description
+ });
+ }
+ }
+
+ return Task.FromResult(errors);
+ }
+
+ ///
+ /// Internal method to validate field types and constraints
+ ///
+ private Task> ValidateFieldTypesInternal(
+ TokenStandardProfile profile,
+ Dictionary metadataDict)
+ {
+ var errors = new List();
+ var allFields = profile.RequiredFields.Concat(profile.OptionalFields).ToList();
+
+ foreach (var field in allFields)
+ {
+ if (!metadataDict.ContainsKey(field.Name) || metadataDict[field.Name] == null)
+ {
+ continue; // Skip validation for missing optional fields
+ }
+
+ var value = metadataDict[field.Name];
+ var fieldErrors = ValidateFieldValue(field, value);
+ errors.AddRange(fieldErrors);
+ }
+
+ return Task.FromResult(errors);
+ }
+
+ ///
+ /// Validates a single field value against its definition
+ ///
+ private List ValidateFieldValue(StandardFieldDefinition field, object value)
+ {
+ var errors = new List();
+
+ // Type validation
+ var expectedType = field.DataType.ToLowerInvariant();
+ var actualType = GetValueType(value);
+
+ if (!IsTypeCompatible(expectedType, actualType, value))
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.METADATA_FIELD_TYPE_MISMATCH,
+ Field = field.Name,
+ Message = $"Field '{field.Name}' expects type '{field.DataType}' but got '{actualType}'",
+ Severity = TokenValidationSeverity.Error
+ });
+ return errors; // Skip further validation if type is wrong
+ }
+
+ // String-specific validation
+ if (expectedType == "string" && value is string strValue)
+ {
+ if (field.MaxLength.HasValue && strValue.Length > field.MaxLength.Value)
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.METADATA_FIELD_VALIDATION_FAILED,
+ Field = field.Name,
+ Message = $"Field '{field.Name}' exceeds maximum length of {field.MaxLength.Value}",
+ Severity = TokenValidationSeverity.Error
+ });
+ }
+
+ if (!string.IsNullOrEmpty(field.ValidationPattern))
+ {
+ try
+ {
+ if (!Regex.IsMatch(strValue, field.ValidationPattern))
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.METADATA_FIELD_VALIDATION_FAILED,
+ Field = field.Name,
+ Message = $"Field '{field.Name}' does not match required pattern",
+ Severity = TokenValidationSeverity.Error,
+ Details = $"Expected pattern: {field.ValidationPattern}"
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Invalid regex pattern for field {FieldName}: {Pattern}",
+ LoggingHelper.SanitizeLogInput(field.Name),
+ LoggingHelper.SanitizeLogInput(field.ValidationPattern));
+ }
+ }
+ }
+
+ // Numeric-specific validation
+ if ((expectedType == "number" || expectedType == "integer") && IsNumeric(value))
+ {
+ var numValue = Convert.ToDouble(value);
+
+ if (field.MinValue.HasValue && numValue < field.MinValue.Value)
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.METADATA_FIELD_VALIDATION_FAILED,
+ Field = field.Name,
+ Message = $"Field '{field.Name}' is below minimum value of {field.MinValue.Value}",
+ Severity = TokenValidationSeverity.Error
+ });
+ }
+
+ if (field.MaxValue.HasValue && numValue > field.MaxValue.Value)
+ {
+ errors.Add(new ValidationError
+ {
+ Code = ErrorCodes.METADATA_FIELD_VALIDATION_FAILED,
+ Field = field.Name,
+ Message = $"Field '{field.Name}' exceeds maximum value of {field.MaxValue.Value}",
+ Severity = TokenValidationSeverity.Error
+ });
+ }
+ }
+
+ return errors;
+ }
+
+ ///
+ /// Validates custom rules from the standard profile
+ ///
+ private Task> ValidateCustomRulesAsync(
+ TokenStandardProfile profile,
+ Dictionary metadataDict)
+ {
+ var errors = new List();
+
+ foreach (var rule in profile.ValidationRules)
+ {
+ var ruleError = ApplyCustomRule(rule, profile, metadataDict);
+ if (ruleError != null)
+ {
+ errors.Add(ruleError);
+ }
+ }
+
+ return Task.FromResult(errors);
+ }
+
+ ///
+ /// Applies a custom validation rule
+ ///
+ private ValidationError? ApplyCustomRule(
+ ValidationRule rule,
+ TokenStandardProfile profile,
+ Dictionary metadataDict)
+ {
+ // ARC-3 specific rules
+ if (profile.Standard == TokenStandard.ARC3)
+ {
+ if (rule.Id == "arc3-image-mimetype")
+ {
+ if (metadataDict.ContainsKey("image") && metadataDict.ContainsKey("image_mimetype"))
+ {
+ var mimetype = metadataDict["image_mimetype"]?.ToString();
+ if (!string.IsNullOrEmpty(mimetype) && !mimetype.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "image_mimetype",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ }
+ else if (rule.Id == "arc3-background-color")
+ {
+ if (metadataDict.ContainsKey("background_color"))
+ {
+ var color = metadataDict["background_color"]?.ToString();
+ if (!string.IsNullOrEmpty(color) && !Regex.IsMatch(color, @"^[0-9A-Fa-f]{6}$"))
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "background_color",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ }
+ }
+
+ // ARC-19 specific rules
+ if (profile.Standard == TokenStandard.ARC19)
+ {
+ if (rule.Id == "arc19-name-length" && metadataDict.ContainsKey("name"))
+ {
+ var name = metadataDict["name"]?.ToString();
+ if (!string.IsNullOrEmpty(name) && name.Length > 32)
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "name",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ else if (rule.Id == "arc19-unit-name-length" && metadataDict.ContainsKey("unit_name"))
+ {
+ var unitName = metadataDict["unit_name"]?.ToString();
+ if (!string.IsNullOrEmpty(unitName) && unitName.Length > 8)
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "unit_name",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ }
+
+ // ARC-69 specific rules
+ if (profile.Standard == TokenStandard.ARC69)
+ {
+ if (rule.Id == "arc69-standard-field" && metadataDict.ContainsKey("standard"))
+ {
+ var standardValue = metadataDict["standard"]?.ToString();
+ if (!string.IsNullOrEmpty(standardValue) && !standardValue.Equals("arc69", StringComparison.OrdinalIgnoreCase))
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "standard",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ }
+
+ // ERC-20 specific rules
+ if (profile.Standard == TokenStandard.ERC20)
+ {
+ if (rule.Id == "erc20-symbol-length" && metadataDict.ContainsKey("symbol"))
+ {
+ var symbol = metadataDict["symbol"]?.ToString();
+ if (!string.IsNullOrEmpty(symbol) && symbol.Length > 11)
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "symbol",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ else if (rule.Id == "erc20-decimals-range" && metadataDict.ContainsKey("decimals"))
+ {
+ if (IsNumeric(metadataDict["decimals"]))
+ {
+ var decimals = Convert.ToInt32(metadataDict["decimals"]);
+ if (decimals < 0 || decimals > 18)
+ {
+ return new ValidationError
+ {
+ Code = rule.ErrorCode,
+ Field = "decimals",
+ Message = rule.ErrorMessage,
+ Severity = rule.Severity
+ };
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Converts metadata object to dictionary
+ ///
+ private Dictionary ConvertToDictionary(object? metadata)
+ {
+ if (metadata == null)
+ {
+ return new Dictionary();
+ }
+
+ if (metadata is Dictionary dict)
+ {
+ return dict;
+ }
+
+ if (metadata is IDictionary iDict)
+ {
+ return new Dictionary(iDict);
+ }
+
+ // Try to convert using JSON serialization
+ try
+ {
+ var json = JsonSerializer.Serialize(metadata);
+ var result = JsonSerializer.Deserialize>(json);
+ return result ?? new Dictionary();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to convert metadata to dictionary");
+ return new Dictionary();
+ }
+ }
+
+ ///
+ /// Gets the type name of a value
+ ///
+ private string GetValueType(object value)
+ {
+ if (value == null) return "null";
+ if (value is string) return "string";
+ if (value is bool) return "boolean";
+ if (IsNumeric(value)) return "number";
+ if (value is Array || value is System.Collections.IList) return "array";
+ if (value is IDictionary || value is JsonElement) return "object";
+ return value.GetType().Name.ToLowerInvariant();
+ }
+
+ ///
+ /// Checks if a value is numeric
+ ///
+ private bool IsNumeric(object value)
+ {
+ return value is byte || value is sbyte ||
+ value is short || value is ushort ||
+ value is int || value is uint ||
+ value is long || value is ulong ||
+ value is float || value is double ||
+ value is decimal;
+ }
+
+ ///
+ /// Checks if actual type is compatible with expected type
+ ///
+ private bool IsTypeCompatible(string expectedType, string actualType, object value)
+ {
+ if (expectedType == actualType) return true;
+
+ // Allow integer for number
+ if (expectedType == "number" && IsNumeric(value)) return true;
+ if (expectedType == "integer" && IsNumeric(value)) return true;
+
+ // Allow JsonElement objects
+ if (value is JsonElement jsonElement)
+ {
+ return expectedType switch
+ {
+ "string" => jsonElement.ValueKind == JsonValueKind.String,
+ "number" => jsonElement.ValueKind == JsonValueKind.Number,
+ "integer" => jsonElement.ValueKind == JsonValueKind.Number,
+ "boolean" => jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False,
+ "array" => jsonElement.ValueKind == JsonValueKind.Array,
+ "object" => jsonElement.ValueKind == JsonValueKind.Object,
+ _ => false
+ };
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/BiatecTokensApi/doc/documentation.xml b/BiatecTokensApi/doc/documentation.xml
index 876c476..bfa69f2 100644
--- a/BiatecTokensApi/doc/documentation.xml
+++ b/BiatecTokensApi/doc/documentation.xml
@@ -2626,6 +2626,58 @@
The correlation ID to add
The enhanced action result
+
+
+ Controller for token standard compliance and validation endpoints
+
+
+
+
+ Initializes a new instance of the TokenStandardsController
+
+ Token standard registry service
+ Token standard validator service
+ Logger instance
+
+
+
+ Gets all supported token standards and their requirements
+
+ Optional filters for standard retrieval
+ List of supported token standard profiles
+
+ This endpoint returns a comprehensive list of all supported token standards,
+ including their version, required fields, optional fields, and validation rules.
+ Use this endpoint to discover what standards are available and what fields are
+ required for each standard when creating tokens.
+
+
+
+
+ Gets details for a specific token standard
+
+ The token standard to retrieve
+ Token standard profile details
+
+ Returns detailed information about a specific token standard, including all
+ required and optional fields, validation rules, and example metadata.
+
+
+
+
+ Validates token metadata against a specified standard (preflight validation)
+
+ Validation request with metadata and standard
+ Validation result with any errors or warnings
+
+ This endpoint performs preflight validation of token metadata against a specified
+ standard without creating a token. Use this to validate metadata before submitting
+ a full token creation request. The response includes detailed error messages and
+ field-specific validation feedback.
+
+ **Performance:** Target p95 latency is under 200ms for typical payloads.
+
+
Provides endpoints for managing compliance webhook subscriptions
@@ -11016,6 +11068,36 @@
Recovery cooldown active
+
+
+ Token metadata validation failed
+
+
+
+
+ Invalid token standard specified
+
+
+
+
+ Required metadata field missing
+
+
+
+
+ Metadata field type mismatch
+
+
+
+
+ Metadata field validation failed
+
+
+
+
+ Token standard not supported
+
+
Represents the response of an Ethereum Virtual Machine (EVM) token deployment operation.
@@ -12600,6 +12682,41 @@
Correlation ID for related events
+
+
+ Token standard profile used for validation (e.g., "ARC3", "ERC20", "Baseline")
+
+
+
+
+ Version of the token standard profile used
+
+
+
+
+ Whether metadata validation was performed
+
+
+
+
+ Result of metadata validation
+
+
+
+
+ Validation error messages if validation failed
+
+
+
+
+ Validation warning messages
+
+
+
+
+ Timestamp when validation was performed
+
+
Request to retrieve token issuance audit logs with filtering
@@ -12655,6 +12772,436 @@
Page size for pagination (default: 50, max: 100)
+
+
+ Request for standards discovery endpoint
+
+
+
+
+ Optional filter to only return active standards
+
+
+
+
+ Optional filter by specific standard
+
+
+
+
+ Response containing list of supported token standards
+
+
+
+
+ List of available token standard profiles
+
+
+
+
+ Total number of standards available
+
+
+
+
+ Request to validate token metadata against a standard
+
+
+
+
+ Token standard to validate against
+
+
+
+
+ Token metadata as JSON object
+
+
+
+
+ Token type (optional, for context)
+
+
+
+
+ Token name
+
+
+
+
+ Token symbol
+
+
+
+
+ Number of decimals
+
+
+
+
+ Total supply
+
+
+
+
+ Response from metadata validation
+
+
+
+
+ Whether validation passed
+
+
+
+
+ Validation result details
+
+
+
+
+ Error code if validation failed
+
+
+
+
+ Human-readable message
+
+
+
+
+ Correlation ID for tracking
+
+
+
+
+ Enumeration of supported token standards for validation and compliance
+
+
+
+
+ Baseline standard with minimal validation requirements
+
+
+
+
+ ARC-3 standard for Algorand tokens with rich metadata
+
+
+
+
+ ARC-19 standard for Algorand tokens with on-chain metadata
+
+
+
+
+ ARC-69 standard for Algorand tokens with simplified metadata
+
+
+
+
+ ERC-20 standard for Ethereum-compatible tokens
+
+
+
+
+ Token standard profile defining validation rules and requirements
+
+
+
+
+ Unique identifier for the standard profile
+
+
+
+
+ Display name of the standard
+
+
+
+
+ Version of the standard profile
+
+
+
+
+ Human-readable description of the standard
+
+
+
+
+ Token standard enumeration value
+
+
+
+
+ List of required metadata fields for this standard
+
+
+
+
+ List of optional metadata fields for this standard
+
+
+
+
+ Validation rules for this standard
+
+
+
+
+ Whether this profile is currently active and available for use
+
+
+
+
+ Example metadata JSON for this standard
+
+
+
+
+ External reference URL for standard specification
+
+
+
+
+ Definition of a metadata field for a token standard
+
+
+
+
+ Field name
+
+
+
+
+ Expected data type (string, number, boolean, object, array)
+
+
+
+
+ Human-readable description of the field
+
+
+
+
+ Default value if not provided (for optional fields)
+
+
+
+
+ Regular expression pattern for validation (for string types)
+
+
+
+
+ Minimum value (for numeric types)
+
+
+
+
+ Maximum value (for numeric types)
+
+
+
+
+ Maximum length (for string types)
+
+
+
+
+ Whether this field is required
+
+
+
+
+ Validation rule for token standard compliance
+
+
+
+
+ Unique identifier for the rule
+
+
+
+
+ Human-readable name of the rule
+
+
+
+
+ Description of what this rule validates
+
+
+
+
+ Error message to display when validation fails
+
+
+
+
+ Error code for programmatic handling
+
+
+
+
+ Severity level of the validation rule
+
+
+
+
+ Severity levels for token validation rules
+
+
+
+
+ Informational message, does not prevent token creation
+
+
+
+
+ Warning message, may indicate potential issues
+
+
+
+
+ Error that must be fixed before token creation
+
+
+
+
+ Result of token metadata validation against a standard profile
+
+
+
+
+ Whether the validation passed
+
+
+
+
+ Token standard that was validated against
+
+
+
+
+ Version of the standard profile used for validation
+
+
+
+
+ List of validation errors encountered
+
+
+
+
+ List of validation warnings
+
+
+
+
+ Timestamp when validation was performed
+
+
+
+
+ Summary message describing validation status
+
+
+
+
+ Individual validation error or warning
+
+
+
+
+ Error code for programmatic handling
+
+
+
+
+ Field name that failed validation
+
+
+
+
+ Human-readable error message
+
+
+
+
+ Severity level
+
+
+
+
+ Additional context about the error
+
+
+
+
+ Token metadata with validation status
+
+
+
+
+ Selected token standard profile
+
+
+
+
+ Version of the standard profile
+
+
+
+
+ Current validation status
+
+
+
+
+ Last validation timestamp
+
+
+
+
+ Validation result message
+
+
+
+
+ Correlation ID for tracking validation through logs
+
+
+
+
+ Validation status enumeration
+
+
+
+
+ Not yet validated
+
+
+
+
+ Validation in progress
+
+
+
+
+ Validation passed successfully
+
+
+
+
+ Validation failed with errors
+
+
+
+
+ Validation passed with warnings
+
+
Represents the various types of tokens supported by the system.
@@ -17681,6 +18228,77 @@
Remaining capacity (-1 for unlimited)
+
+
+ Service for managing and retrieving token standard profiles
+
+
+
+
+ Gets all available token standard profiles
+
+ If true, only returns active profiles
+ List of token standard profiles
+
+
+
+ Gets a specific token standard profile by standard type
+
+ The token standard to retrieve
+ Token standard profile or null if not found
+
+
+
+ Gets the default token standard for backward compatibility
+
+ Default token standard profile
+
+
+
+ Checks if a token standard is supported
+
+ The token standard to check
+ True if supported, false otherwise
+
+
+
+ Service for validating token metadata against standards
+
+
+
+
+ Validates token metadata against a specified standard profile
+
+ The token standard to validate against
+ The metadata object to validate
+ Optional token name for context
+ Optional token symbol for context
+ Optional decimals for context
+ Validation result with errors and warnings
+
+
+
+ Validates that required fields are present
+
+ The standard profile to validate against
+ The metadata object to validate
+ List of validation errors for missing required fields
+
+
+
+ Validates field types and constraints
+
+ The standard profile to validate against
+ The metadata object to validate
+ List of validation errors for field type mismatches
+
+
+
+ Checks if the validator supports a given standard
+
+ The token standard to check
+ True if supported, false otherwise
+
Service interface for webhook operations
@@ -18062,6 +18680,144 @@
+
+
+ Registry service for managing token standard profiles
+
+
+
+
+ Initializes a new instance of the TokenStandardRegistry
+
+ Logger instance
+
+
+
+ Gets all available token standard profiles
+
+
+
+
+ Gets a specific token standard profile by standard type
+
+
+
+
+ Gets the default token standard for backward compatibility
+
+
+
+
+ Checks if a token standard is supported
+
+
+
+
+ Initializes all supported token standard profiles
+
+
+
+
+ Creates the Baseline standard profile
+
+
+
+
+ Creates the ARC-3 standard profile
+
+
+
+
+ Creates the ARC-19 standard profile
+
+
+
+
+ Creates the ARC-69 standard profile
+
+
+
+
+ Creates the ERC-20 standard profile
+
+
+
+
+ Service for validating token metadata against standards
+
+
+
+
+ Initializes a new instance of the TokenStandardValidator
+
+ Logger instance
+ Token standard registry
+
+
+
+ Validates token metadata against a specified standard profile
+
+
+
+
+ Validates that required fields are present
+
+
+
+
+ Validates field types and constraints
+
+
+
+
+ Checks if the validator supports a given standard
+
+
+
+
+ Internal method to validate required fields
+
+
+
+
+ Internal method to validate field types and constraints
+
+
+
+
+ Validates a single field value against its definition
+
+
+
+
+ Validates custom rules from the standard profile
+
+
+
+
+ Applies a custom validation rule
+
+
+
+
+ Converts metadata object to dictionary
+
+
+
+
+ Gets the type name of a value
+
+
+
+
+ Checks if a value is numeric
+
+
+
+
+ Checks if actual type is compatible with expected type
+
+
Service for managing webhook subscriptions and event delivery
diff --git a/BiatecTokensTests/TokenStandardRegistryTests.cs b/BiatecTokensTests/TokenStandardRegistryTests.cs
new file mode 100644
index 0000000..8b33abe
--- /dev/null
+++ b/BiatecTokensTests/TokenStandardRegistryTests.cs
@@ -0,0 +1,282 @@
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+
+namespace BiatecTokensTests
+{
+ ///
+ /// Unit tests for TokenStandardRegistry service
+ ///
+ [TestFixture]
+ public class TokenStandardRegistryTests
+ {
+ private Mock> _loggerMock;
+ private TokenStandardRegistry _registry;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _loggerMock = new Mock>();
+ _registry = new TokenStandardRegistry(_loggerMock.Object);
+ }
+
+ [Test]
+ public async Task GetAllStandardsAsync_ReturnsActiveStandards()
+ {
+ // Act
+ var result = await _registry.GetAllStandardsAsync(activeOnly: true);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result, Is.Not.Empty);
+ Assert.That(result.All(p => p.IsActive), Is.True);
+ }
+
+ [Test]
+ public async Task GetAllStandardsAsync_ReturnsAllStandards_WhenActiveOnlyFalse()
+ {
+ // Act
+ var result = await _registry.GetAllStandardsAsync(activeOnly: false);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result, Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task GetAllStandardsAsync_ContainsExpectedStandards()
+ {
+ // Act
+ var result = await _registry.GetAllStandardsAsync();
+
+ // Assert
+ var standards = result.Select(p => p.Standard).ToList();
+ Assert.That(standards, Does.Contain(TokenStandard.Baseline));
+ Assert.That(standards, Does.Contain(TokenStandard.ARC3));
+ Assert.That(standards, Does.Contain(TokenStandard.ARC19));
+ Assert.That(standards, Does.Contain(TokenStandard.ARC69));
+ Assert.That(standards, Does.Contain(TokenStandard.ERC20));
+ }
+
+ [Test]
+ public async Task GetStandardProfileAsync_ReturnsProfile_ForValidStandard()
+ {
+ // Act
+ var result = await _registry.GetStandardProfileAsync(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result.Standard, Is.EqualTo(TokenStandard.ARC3));
+ Assert.That(result.Name, Is.EqualTo("ARC-3"));
+ Assert.That(result.Version, Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task GetStandardProfileAsync_ReturnsNull_ForInactiveStandard()
+ {
+ // Act - requesting a standard that doesn't exist will return null
+ var result = await _registry.GetStandardProfileAsync((TokenStandard)999);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [TestCase(TokenStandard.Baseline)]
+ [TestCase(TokenStandard.ARC3)]
+ [TestCase(TokenStandard.ARC19)]
+ [TestCase(TokenStandard.ARC69)]
+ [TestCase(TokenStandard.ERC20)]
+ public async Task GetStandardProfileAsync_ReturnsValidProfile_ForEachStandard(TokenStandard standard)
+ {
+ // Act
+ var result = await _registry.GetStandardProfileAsync(standard);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result!.Standard, Is.EqualTo(standard));
+ Assert.That(result.Name, Is.Not.Empty);
+ Assert.That(result.Version, Is.Not.Empty);
+ Assert.That(result.Description, Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task GetDefaultStandardAsync_ReturnsBaselineProfile()
+ {
+ // Act
+ var result = await _registry.GetDefaultStandardAsync();
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result.Standard, Is.EqualTo(TokenStandard.Baseline));
+ Assert.That(result.IsActive, Is.True);
+ }
+
+ [TestCase(TokenStandard.Baseline, true)]
+ [TestCase(TokenStandard.ARC3, true)]
+ [TestCase(TokenStandard.ARC19, true)]
+ [TestCase(TokenStandard.ARC69, true)]
+ [TestCase(TokenStandard.ERC20, true)]
+ public async Task IsStandardSupportedAsync_ReturnsExpectedResult(TokenStandard standard, bool expectedSupported)
+ {
+ // Act
+ var result = await _registry.IsStandardSupportedAsync(standard);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(expectedSupported));
+ }
+
+ [Test]
+ public async Task ARC3Profile_HasRequiredFields()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.RequiredFields.Any(f => f.Name == "name"), Is.True);
+ }
+
+ [Test]
+ public async Task ARC3Profile_HasOptionalFields()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.OptionalFields, Is.Not.Empty);
+ Assert.That(profile.OptionalFields.Any(f => f.Name == "image"), Is.True);
+ Assert.That(profile.OptionalFields.Any(f => f.Name == "description"), Is.True);
+ Assert.That(profile.OptionalFields.Any(f => f.Name == "properties"), Is.True);
+ }
+
+ [Test]
+ public async Task ARC3Profile_HasValidationRules()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.ValidationRules, Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task ARC19Profile_HasNameLengthConstraint()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ARC19);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ var nameField = profile!.RequiredFields.FirstOrDefault(f => f.Name == "name");
+ Assert.That(nameField, Is.Not.Null);
+ Assert.That(nameField!.MaxLength, Is.EqualTo(32));
+ }
+
+ [Test]
+ public async Task ARC69Profile_RequiresStandardField()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ARC69);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.RequiredFields.Any(f => f.Name == "standard"), Is.True);
+ }
+
+ [Test]
+ public async Task ERC20Profile_HasRequiredMetadataFields()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ERC20);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.RequiredFields.Any(f => f.Name == "name"), Is.True);
+ Assert.That(profile.RequiredFields.Any(f => f.Name == "symbol"), Is.True);
+ Assert.That(profile.RequiredFields.Any(f => f.Name == "decimals"), Is.True);
+ }
+
+ [Test]
+ public async Task ERC20Profile_SymbolHasMaxLength()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ERC20);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ var symbolField = profile!.RequiredFields.FirstOrDefault(f => f.Name == "symbol");
+ Assert.That(symbolField, Is.Not.Null);
+ Assert.That(symbolField!.MaxLength, Is.EqualTo(11));
+ }
+
+ [Test]
+ public async Task ERC20Profile_DecimalsHasRange()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.ERC20);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ var decimalsField = profile!.RequiredFields.FirstOrDefault(f => f.Name == "decimals");
+ Assert.That(decimalsField, Is.Not.Null);
+ Assert.That(decimalsField!.MinValue, Is.EqualTo(0));
+ Assert.That(decimalsField.MaxValue, Is.EqualTo(18));
+ }
+
+ [Test]
+ public async Task BaselineProfile_HasMinimalRequirements()
+ {
+ // Act
+ var profile = await _registry.GetStandardProfileAsync(TokenStandard.Baseline);
+
+ // Assert
+ Assert.That(profile, Is.Not.Null);
+ Assert.That(profile!.RequiredFields, Has.Count.EqualTo(1));
+ Assert.That(profile.RequiredFields[0].Name, Is.EqualTo("name"));
+ }
+
+ [Test]
+ public async Task AllProfiles_HaveValidVersionNumbers()
+ {
+ // Act
+ var profiles = await _registry.GetAllStandardsAsync();
+
+ // Assert
+ foreach (var profile in profiles)
+ {
+ Assert.That(profile.Version, Is.Not.Empty);
+ Assert.That(profile.Version, Does.Match(@"^\d+\.\d+\.\d+$"));
+ }
+ }
+
+ [Test]
+ public async Task AllProfiles_HaveUniqueIds()
+ {
+ // Act
+ var profiles = await _registry.GetAllStandardsAsync();
+
+ // Assert
+ var ids = profiles.Select(p => p.Id).ToList();
+ Assert.That(ids.Distinct().Count(), Is.EqualTo(ids.Count));
+ }
+
+ [Test]
+ public async Task AllProfiles_HaveSpecificationUrls()
+ {
+ // Act
+ var profiles = await _registry.GetAllStandardsAsync();
+
+ // Assert - Baseline may not have a spec URL, but others should
+ var profilesWithSpecs = profiles.Where(p => p.Standard != TokenStandard.Baseline);
+ foreach (var profile in profilesWithSpecs)
+ {
+ Assert.That(profile.SpecificationUrl, Is.Not.Null);
+ Assert.That(profile.SpecificationUrl, Does.StartWith("http"));
+ }
+ }
+ }
+}
diff --git a/BiatecTokensTests/TokenStandardValidatorTests.cs b/BiatecTokensTests/TokenStandardValidatorTests.cs
new file mode 100644
index 0000000..a45d028
--- /dev/null
+++ b/BiatecTokensTests/TokenStandardValidatorTests.cs
@@ -0,0 +1,517 @@
+using BiatecTokensApi.Models;
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services;
+using BiatecTokensApi.Services.Interface;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+
+namespace BiatecTokensTests
+{
+ ///
+ /// Unit tests for TokenStandardValidator service
+ ///
+ [TestFixture]
+ public class TokenStandardValidatorTests
+ {
+ private Mock> _loggerMock;
+ private Mock _registryMock;
+ private TokenStandardValidator _validator;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _loggerMock = new Mock>();
+ _registryMock = new Mock();
+ _validator = new TokenStandardValidator(_loggerMock.Object, _registryMock.Object);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ReturnsError_WhenStandardNotSupported()
+ {
+ // Arrange
+ _registryMock.Setup(r => r.GetStandardProfileAsync(It.IsAny()))
+ .ReturnsAsync((TokenStandardProfile?)null);
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC3, new { name = "Test" });
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e => e.Code == ErrorCodes.INVALID_TOKEN_STANDARD), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_PassesValidation_ForValidBaselineMetadata()
+ {
+ // Arrange
+ var profile = CreateBaselineProfile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.Baseline))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "My Token" }
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.Baseline, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.True);
+ Assert.That(result.Errors, Is.Empty);
+ }
+
+ [Test]
+ public async Task ValidateAsync_FailsValidation_WhenRequiredFieldMissing()
+ {
+ // Arrange
+ var profile = CreateBaselineProfile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.Baseline))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary();
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.Baseline, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e =>
+ e.Code == ErrorCodes.REQUIRED_METADATA_FIELD_MISSING && e.Field == "name"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ValidatesStringMaxLength()
+ {
+ // Arrange
+ var profile = CreateBaselineProfile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.Baseline))
+ .ReturnsAsync(profile);
+
+ var longName = new string('A', 300); // Exceeds max length of 256
+ var metadata = new Dictionary
+ {
+ { "name", longName }
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.Baseline, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e =>
+ e.Code == ErrorCodes.METADATA_FIELD_VALIDATION_FAILED && e.Field == "name"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ValidatesNumericRange()
+ {
+ // Arrange
+ var profile = CreateERC20Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ERC20))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Token" },
+ { "symbol", "TKN" },
+ { "decimals", 25 } // Exceeds max of 18
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ERC20, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e =>
+ e.Code == ErrorCodes.METADATA_FIELD_VALIDATION_FAILED && e.Field == "decimals"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ValidatesRegexPattern()
+ {
+ // Arrange
+ var profile = CreateARC3Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC3))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Test Token" },
+ { "background_color", "GGGGGG" } // Invalid hex color
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC3, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e =>
+ e.Code == ErrorCodes.METADATA_FIELD_VALIDATION_FAILED && e.Field == "background_color"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_AcceptsValidRegexPattern()
+ {
+ // Arrange
+ var profile = CreateARC3Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC3))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Test Token" },
+ { "background_color", "FF0000" } // Valid hex color
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC3, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.True);
+ Assert.That(result.Errors, Is.Empty);
+ }
+
+ [Test]
+ public async Task ValidateAsync_IncludesContextFields()
+ {
+ // Arrange
+ var profile = CreateERC20Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ERC20))
+ .ReturnsAsync(profile);
+
+ // Act - Not including required fields in metadata, but passing them as context
+ var result = await _validator.ValidateAsync(
+ TokenStandard.ERC20,
+ null,
+ tokenName: "My Token",
+ tokenSymbol: "MTK",
+ decimals: 6);
+
+ // Assert
+ Assert.That(result.IsValid, Is.True);
+ Assert.That(result.Errors, Is.Empty);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ValidatesTypeCompatibility()
+ {
+ // Arrange
+ var profile = CreateERC20Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ERC20))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Token" },
+ { "symbol", "TKN" },
+ { "decimals", "not a number" } // Wrong type
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ERC20, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e =>
+ e.Code == ErrorCodes.METADATA_FIELD_TYPE_MISMATCH && e.Field == "decimals"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_AppliesARC3CustomRules()
+ {
+ // Arrange
+ var profile = CreateARC3Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC3))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Test" },
+ { "image", "ipfs://test" },
+ { "image_mimetype", "video/mp4" } // Should be image/*
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC3, metadata);
+
+ // Assert
+ Assert.That(result.Warnings.Any(w => w.Code == "ARC3_INVALID_IMAGE_MIMETYPE"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_AppliesARC19CustomRules()
+ {
+ // Arrange
+ var profile = CreateARC19Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC19))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "This is a very long token name that exceeds the limit" }, // > 32 chars
+ { "unit_name", "TKN" }
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC19, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e => e.Code == "ARC19_NAME_TOO_LONG"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_AppliesARC69CustomRules()
+ {
+ // Arrange
+ var profile = CreateARC69Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC69))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "standard", "arc70" } // Wrong value
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC69, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e => e.Code == "ARC69_INVALID_STANDARD_FIELD"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_AppliesERC20CustomRules()
+ {
+ // Arrange
+ var profile = CreateERC20Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ERC20))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Token" },
+ { "symbol", "VERYLONGSYMBOL" }, // > 11 chars
+ { "decimals", 10 }
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ERC20, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.False);
+ Assert.That(result.Errors.Any(e => e.Code == "ERC20_SYMBOL_TOO_LONG"), Is.True);
+ }
+
+ [Test]
+ public async Task ValidateAsync_ReturnsWarnings_WhenValidWithWarnings()
+ {
+ // Arrange
+ var profile = CreateARC3Profile();
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC3))
+ .ReturnsAsync(profile);
+
+ var metadata = new Dictionary
+ {
+ { "name", "Test" },
+ { "image", "ipfs://test" },
+ { "image_mimetype", "application/pdf" } // Warning
+ };
+
+ // Act
+ var result = await _validator.ValidateAsync(TokenStandard.ARC3, metadata);
+
+ // Assert
+ Assert.That(result.IsValid, Is.True); // Still valid despite warnings
+ Assert.That(result.Warnings, Is.Not.Empty);
+ }
+
+ [Test]
+ public async Task SupportsStandard_ReturnsTrue()
+ {
+ // Act
+ var result = _validator.SupportsStandard(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ // Helper methods to create test profiles
+ private TokenStandardProfile CreateBaselineProfile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "baseline-1.0",
+ Name = "Baseline",
+ Version = "1.0.0",
+ Standard = TokenStandard.Baseline,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ IsRequired = true,
+ MaxLength = 256
+ }
+ },
+ ValidationRules = new List()
+ };
+ }
+
+ private TokenStandardProfile CreateARC3Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc3-1.0",
+ Name = "ARC-3",
+ Version = "1.0.0",
+ Standard = TokenStandard.ARC3,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition { Name = "name", DataType = "string", IsRequired = true }
+ },
+ OptionalFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "background_color",
+ DataType = "string",
+ ValidationPattern = @"^[0-9A-Fa-f]{6}$"
+ },
+ new StandardFieldDefinition { Name = "image", DataType = "string" },
+ new StandardFieldDefinition { Name = "image_mimetype", DataType = "string" }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc3-image-mimetype",
+ ErrorCode = "ARC3_INVALID_IMAGE_MIMETYPE",
+ Severity = TokenValidationSeverity.Warning
+ },
+ new ValidationRule
+ {
+ Id = "arc3-background-color",
+ ErrorCode = "ARC3_INVALID_BACKGROUND_COLOR",
+ Severity = TokenValidationSeverity.Error
+ }
+ }
+ };
+ }
+
+ private TokenStandardProfile CreateARC19Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc19-1.0",
+ Name = "ARC-19",
+ Version = "1.0.0",
+ Standard = TokenStandard.ARC19,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ IsRequired = true,
+ MaxLength = 32
+ },
+ new StandardFieldDefinition
+ {
+ Name = "unit_name",
+ DataType = "string",
+ IsRequired = true,
+ MaxLength = 8
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc19-name-length",
+ ErrorCode = "ARC19_NAME_TOO_LONG",
+ Severity = TokenValidationSeverity.Error
+ }
+ }
+ };
+ }
+
+ private TokenStandardProfile CreateARC69Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "arc69-1.0",
+ Name = "ARC-69",
+ Version = "1.0.0",
+ Standard = TokenStandard.ARC69,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition { Name = "standard", DataType = "string", IsRequired = true }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "arc69-standard-field",
+ ErrorCode = "ARC69_INVALID_STANDARD_FIELD",
+ Severity = TokenValidationSeverity.Error
+ }
+ }
+ };
+ }
+
+ private TokenStandardProfile CreateERC20Profile()
+ {
+ return new TokenStandardProfile
+ {
+ Id = "erc20-1.0",
+ Name = "ERC-20",
+ Version = "1.0.0",
+ Standard = TokenStandard.ERC20,
+ RequiredFields = new List
+ {
+ new StandardFieldDefinition
+ {
+ Name = "name",
+ DataType = "string",
+ IsRequired = true
+ },
+ new StandardFieldDefinition
+ {
+ Name = "symbol",
+ DataType = "string",
+ IsRequired = true,
+ MaxLength = 11
+ },
+ new StandardFieldDefinition
+ {
+ Name = "decimals",
+ DataType = "number",
+ IsRequired = true,
+ MinValue = 0,
+ MaxValue = 18
+ }
+ },
+ ValidationRules = new List
+ {
+ new ValidationRule
+ {
+ Id = "erc20-symbol-length",
+ ErrorCode = "ERC20_SYMBOL_TOO_LONG",
+ Severity = TokenValidationSeverity.Error
+ },
+ new ValidationRule
+ {
+ Id = "erc20-decimals-range",
+ ErrorCode = "ERC20_INVALID_DECIMALS",
+ Severity = TokenValidationSeverity.Error
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/BiatecTokensTests/TokenStandardsControllerTests.cs b/BiatecTokensTests/TokenStandardsControllerTests.cs
new file mode 100644
index 0000000..c01350f
--- /dev/null
+++ b/BiatecTokensTests/TokenStandardsControllerTests.cs
@@ -0,0 +1,408 @@
+using BiatecTokensApi.Controllers;
+using BiatecTokensApi.Models.TokenStandards;
+using BiatecTokensApi.Services.Interface;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+
+namespace BiatecTokensTests
+{
+ ///
+ /// Integration tests for TokenStandardsController
+ ///
+ [TestFixture]
+ public class TokenStandardsControllerTests
+ {
+ private Mock _registryMock;
+ private Mock _validatorMock;
+ private Mock> _loggerMock;
+ private TokenStandardsController _controller;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _registryMock = new Mock();
+ _validatorMock = new Mock();
+ _loggerMock = new Mock>();
+ _controller = new TokenStandardsController(
+ _registryMock.Object,
+ _validatorMock.Object,
+ _loggerMock.Object);
+ }
+
+ [Test]
+ public async Task GetStandards_ReturnsOk_WithStandardsList()
+ {
+ // Arrange
+ var standards = new List
+ {
+ new TokenStandardProfile
+ {
+ Id = "baseline-1.0",
+ Standard = TokenStandard.Baseline,
+ Name = "Baseline"
+ },
+ new TokenStandardProfile
+ {
+ Id = "arc3-1.0",
+ Standard = TokenStandard.ARC3,
+ Name = "ARC-3"
+ }
+ };
+ _registryMock.Setup(r => r.GetAllStandardsAsync(It.IsAny()))
+ .ReturnsAsync(standards);
+
+ // Act
+ var result = await _controller.GetStandards(null);
+
+ // Assert
+ Assert.That(result, Is.TypeOf());
+ var okResult = (OkObjectResult)result;
+ Assert.That(okResult.Value, Is.TypeOf());
+ var response = (GetTokenStandardsResponse)okResult.Value;
+ Assert.That(response.TotalCount, Is.EqualTo(2));
+ Assert.That(response.Standards.Count, Is.EqualTo(2));
+ }
+
+ [Test]
+ public async Task GetStandards_FiltersActiveOnly()
+ {
+ // Arrange
+ var activeStandards = new List
+ {
+ new TokenStandardProfile
+ {
+ Standard = TokenStandard.Baseline,
+ IsActive = true
+ }
+ };
+ _registryMock.Setup(r => r.GetAllStandardsAsync(true))
+ .ReturnsAsync(activeStandards);
+
+ var request = new GetTokenStandardsRequest { ActiveOnly = true };
+
+ // Act
+ var result = await _controller.GetStandards(request);
+
+ // Assert
+ Assert.That(result, Is.TypeOf());
+ var okResult = (OkObjectResult)result;
+ Assert.That(okResult.Value, Is.TypeOf());
+ var response = (GetTokenStandardsResponse)okResult.Value;
+ foreach (var standard in response.Standards)
+ {
+ Assert.That(standard.IsActive, Is.True);
+ }
+ }
+
+ [Test]
+ public async Task GetStandards_FiltersSpecificStandard()
+ {
+ // Arrange
+ var allStandards = new List
+ {
+ new TokenStandardProfile
+ {
+ Standard = TokenStandard.ARC3,
+ Name = "ARC-3"
+ },
+ new TokenStandardProfile
+ {
+ Standard = TokenStandard.Baseline,
+ Name = "Baseline"
+ }
+ };
+ _registryMock.Setup(r => r.GetAllStandardsAsync(It.IsAny()))
+ .ReturnsAsync(allStandards);
+
+ var request = new GetTokenStandardsRequest { Standard = TokenStandard.ARC3 };
+
+ // Act
+ var result = await _controller.GetStandards(request);
+
+ // Assert
+ Assert.That(result, Is.TypeOf());
+ var okResult = (OkObjectResult)result;
+ Assert.That(okResult.Value, Is.TypeOf());
+ var response = (GetTokenStandardsResponse)okResult.Value;
+ Assert.That(response.Standards, Has.Count.EqualTo(1));
+ Assert.That(response.Standards[0].Standard, Is.EqualTo(TokenStandard.ARC3));
+ }
+
+ [Test]
+ public async Task GetStandard_ReturnsOk_ForValidStandard()
+ {
+ // Arrange
+ var profile = new TokenStandardProfile
+ {
+ Id = "arc3-1.0",
+ Standard = TokenStandard.ARC3,
+ Name = "ARC-3",
+ Version = "1.0.0"
+ };
+ _registryMock.Setup(r => r.GetStandardProfileAsync(TokenStandard.ARC3))
+ .ReturnsAsync(profile);
+
+ // Act
+ var result = await _controller.GetStandard(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(result, Is.TypeOf());
+ var okResult = (OkObjectResult)result;
+ Assert.That(okResult.Value, Is.TypeOf());
+ var returnedProfile = (TokenStandardProfile)okResult.Value;
+ Assert.That(returnedProfile.Standard, Is.EqualTo(TokenStandard.ARC3));
+ }
+
+ [Test]
+ public async Task GetStandard_ReturnsNotFound_ForUnsupportedStandard()
+ {
+ // Arrange
+ _registryMock.Setup(r => r.GetStandardProfileAsync(It.IsAny()))
+ .ReturnsAsync((TokenStandardProfile?)null);
+
+ // Act
+ var result = await _controller.GetStandard(TokenStandard.ARC3);
+
+ // Assert
+ Assert.That(result, Is.TypeOf());
+ }
+
+ [Test]
+ public async Task ValidateMetadata_ReturnsOk_ForValidMetadata()
+ {
+ // Arrange
+ _registryMock.Setup(r => r.IsStandardSupportedAsync(TokenStandard.ARC3))
+ .ReturnsAsync(true);
+
+ var validationResult = new TokenValidationResult
+ {
+ IsValid = true,
+ Standard = TokenStandard.ARC3,
+ StandardVersion = "1.0.0",
+ Message = "Validation passed successfully"
+ };
+ _validatorMock.Setup(v => v.ValidateAsync(
+ It.IsAny(),
+ It.IsAny