diff --git a/catalyst-gateway/bin/src/service/common/objects/cardano/registration_info.rs b/catalyst-gateway/bin/src/service/common/objects/cardano/registration_info.rs index 9560e7da983..a6161769483 100644 --- a/catalyst-gateway/bin/src/service/common/objects/cardano/registration_info.rs +++ b/catalyst-gateway/bin/src/service/common/objects/cardano/registration_info.rs @@ -20,13 +20,30 @@ struct Delegation { power: i64, } +/// Represents a list of delegations +#[derive(Object)] +struct Delegations { + /// A list of delegations. + #[oai(validator(max_items = "100"))] + delegations: Vec, +} + +/// Direct voter type +#[derive(Object)] +struct DirectVoter { + /// Voting key. + #[oai(validator(min_length = "66", max_length = "66", pattern = "0x[0-9a-f]{64}"))] + voting_key: String, +} + /// Voting key type #[derive(Union)] +#[oai(discriminator_name = "type", one_of = true)] enum VotingInfo { /// direct voting key - Direct(String), + Direct(DirectVoter), /// delegations - Delegated(Vec), + Delegated(Delegations), } /// User's [CIP-36](https://cips.cardano.org/cip/CIP-36/) registration info. @@ -57,11 +74,13 @@ impl RegistrationInfo { ) -> Self { let voting_info = match voting_info { PublicVotingInfo::Direct(voting_key) => { - VotingInfo::Direct(to_hex_with_prefix(voting_key.bytes())) + VotingInfo::Direct(DirectVoter { + voting_key: to_hex_with_prefix(voting_key.bytes()), + }) }, PublicVotingInfo::Delegated(delegations) => { - VotingInfo::Delegated( - delegations + VotingInfo::Delegated(Delegations { + delegations: delegations .into_iter() .map(|(voting_key, power)| { Delegation { @@ -70,7 +89,7 @@ impl RegistrationInfo { } }) .collect(), - ) + }) }, }; Self { @@ -94,11 +113,14 @@ impl Example for RegistrationInfo { .expect("Invalid hex") .into(), nonce: 11_623_850, - voting_info: VotingInfo::Delegated(vec![Delegation { - voting_key: "0xb16f03d67e95ddd321df4bee8658901eb183d4cb5623624ff5edd7fe54f8e857" - .to_string(), - power: 1, - }]), + voting_info: VotingInfo::Delegated(Delegations { + delegations: vec![Delegation { + voting_key: + "0xb16f03d67e95ddd321df4bee8658901eb183d4cb5623624ff5edd7fe54f8e857" + .to_string(), + power: 1, + }], + }), } } } diff --git a/catalyst-gateway/tests/.spectral.yml b/catalyst-gateway/tests/.spectral.yml index 5d03e95b9fd..b5eefe860c2 100644 --- a/catalyst-gateway/tests/.spectral.yml +++ b/catalyst-gateway/tests/.spectral.yml @@ -51,6 +51,8 @@ overrides: # warn: Should be implemented, but is blocked by a technical issue. # info: Good to be implemented. + # Ref: https://github.com/stoplightio/spectral-owasp-ruleset/blob/2fd49c377794222352ff10dee99ed2a106c35199/src/ruleset.ts#L767 + owasp:api7:2019-security-hosts-https-oas3: info # Rate limit # Ref: https://github.com/stoplightio/spectral-owasp-ruleset/blob/2fd49c377794222352ff10dee99ed2a106c35199/src/ruleset.ts#L436 owasp:api4:2019-rate-limit: warn diff --git a/catalyst-gateway/tests/Earthfile b/catalyst-gateway/tests/Earthfile index 0c07b4f3e65..7cc64788357 100644 --- a/catalyst-gateway/tests/Earthfile +++ b/catalyst-gateway/tests/Earthfile @@ -62,9 +62,8 @@ fuzzer-api: # test-lint-openapi - OpenAPI linting from an artifact # testing whether the OpenAPI generated during build stage follows good practice. -# TODO: https://github.com/input-output-hk/catalyst-voices/issues/408 # enable this linting after this issue will be solved -lint-openapi: +test-lint-openapi: FROM github.com/input-output-hk/catalyst-ci/earthly/spectral:v2.4.0+spectral-base # Copy the doc artifact. COPY ../+build/doc ./doc diff --git a/catalyst-gateway/tests/api_tests/api_tests/test_voter_registration.py b/catalyst-gateway/tests/api_tests/api_tests/test_voter_registration.py index 5b969dea473..d42961378d7 100644 --- a/catalyst-gateway/tests/api_tests/api_tests/test_voter_registration.py +++ b/catalyst-gateway/tests/api_tests/api_tests/test_voter_registration.py @@ -11,17 +11,18 @@ def check_delegations(provided, expected): if type(expected) is list: + provided_delegations = provided["delegations"] for i in range(0, len(expected)): expected_voting_key = expected[i][0] expected_power = expected[i][1] - provided_voting_key = provided[i]["voting_key"] - provided_power = provided[i]["power"] + provided_voting_key = provided_delegations[i]["voting_key"] + provided_power = provided_delegations[i]["power"] assert ( expected_voting_key == provided_voting_key and expected_power == provided_power ) else: - assert provided == expected + assert provided["voting_key"] == expected def test_voter_registration_endpoint(): diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart index 546441f8f24..199cba5729f 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.dart @@ -210,6 +210,95 @@ extension $DelegationExtension on Delegation { } } +@JsonSerializable(explicitToJson: true) +class Delegations { + const Delegations({ + required this.delegations, + }); + + factory Delegations.fromJson(Map json) => + _$DelegationsFromJson(json); + + static const toJsonFactory = _$DelegationsToJson; + Map toJson() => _$DelegationsToJson(this); + + @JsonKey(name: 'delegations', defaultValue: []) + final List delegations; + static const fromJsonFactory = _$DelegationsFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is Delegations && + (identical(other.delegations, delegations) || + const DeepCollectionEquality() + .equals(other.delegations, delegations))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(delegations) ^ runtimeType.hashCode; +} + +extension $DelegationsExtension on Delegations { + Delegations copyWith({List? delegations}) { + return Delegations(delegations: delegations ?? this.delegations); + } + + Delegations copyWithWrapped({Wrapped>? delegations}) { + return Delegations( + delegations: + (delegations != null ? delegations.value : this.delegations)); + } +} + +@JsonSerializable(explicitToJson: true) +class DirectVoter { + const DirectVoter({ + required this.votingKey, + }); + + factory DirectVoter.fromJson(Map json) => + _$DirectVoterFromJson(json); + + static const toJsonFactory = _$DirectVoterToJson; + Map toJson() => _$DirectVoterToJson(this); + + @JsonKey(name: 'voting_key') + final String votingKey; + static const fromJsonFactory = _$DirectVoterFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is DirectVoter && + (identical(other.votingKey, votingKey) || + const DeepCollectionEquality() + .equals(other.votingKey, votingKey))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(votingKey) ^ runtimeType.hashCode; +} + +extension $DirectVoterExtension on DirectVoter { + DirectVoter copyWith({String? votingKey}) { + return DirectVoter(votingKey: votingKey ?? this.votingKey); + } + + DirectVoter copyWithWrapped({Wrapped? votingKey}) { + return DirectVoter( + votingKey: (votingKey != null ? votingKey.value : this.votingKey)); + } +} + @JsonSerializable(explicitToJson: true) class FragmentStatus { const FragmentStatus(); @@ -1207,7 +1296,135 @@ extension $VoterRegistrationExtension on VoterRegistration { } } -typedef VotingInfo = Map; +@JsonSerializable(explicitToJson: true) +class VotingInfo { + const VotingInfo(); + + factory VotingInfo.fromJson(Map json) => + _$VotingInfoFromJson(json); + + static const toJsonFactory = _$VotingInfoToJson; + Map toJson() => _$VotingInfoToJson(this); + + static const fromJsonFactory = _$VotingInfoFromJson; + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => runtimeType.hashCode; +} + +@JsonSerializable(explicitToJson: true) +class VotingInfoDelegations { + const VotingInfoDelegations({ + required this.type, + required this.delegations, + }); + + factory VotingInfoDelegations.fromJson(Map json) => + _$VotingInfoDelegationsFromJson(json); + + static const toJsonFactory = _$VotingInfoDelegationsToJson; + Map toJson() => _$VotingInfoDelegationsToJson(this); + + @JsonKey(name: 'type') + final String type; + @JsonKey(name: 'delegations', defaultValue: []) + final List delegations; + static const fromJsonFactory = _$VotingInfoDelegationsFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is VotingInfoDelegations && + (identical(other.type, type) || + const DeepCollectionEquality().equals(other.type, type)) && + (identical(other.delegations, delegations) || + const DeepCollectionEquality() + .equals(other.delegations, delegations))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(type) ^ + const DeepCollectionEquality().hash(delegations) ^ + runtimeType.hashCode; +} + +extension $VotingInfoDelegationsExtension on VotingInfoDelegations { + VotingInfoDelegations copyWith( + {String? type, List? delegations}) { + return VotingInfoDelegations( + type: type ?? this.type, delegations: delegations ?? this.delegations); + } + + VotingInfoDelegations copyWithWrapped( + {Wrapped? type, Wrapped>? delegations}) { + return VotingInfoDelegations( + type: (type != null ? type.value : this.type), + delegations: + (delegations != null ? delegations.value : this.delegations)); + } +} + +@JsonSerializable(explicitToJson: true) +class VotingInfoDirectVoter { + const VotingInfoDirectVoter({ + required this.type, + required this.votingKey, + }); + + factory VotingInfoDirectVoter.fromJson(Map json) => + _$VotingInfoDirectVoterFromJson(json); + + static const toJsonFactory = _$VotingInfoDirectVoterToJson; + Map toJson() => _$VotingInfoDirectVoterToJson(this); + + @JsonKey(name: 'type') + final String type; + @JsonKey(name: 'voting_key') + final String votingKey; + static const fromJsonFactory = _$VotingInfoDirectVoterFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is VotingInfoDirectVoter && + (identical(other.type, type) || + const DeepCollectionEquality().equals(other.type, type)) && + (identical(other.votingKey, votingKey) || + const DeepCollectionEquality() + .equals(other.votingKey, votingKey))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(type) ^ + const DeepCollectionEquality().hash(votingKey) ^ + runtimeType.hashCode; +} + +extension $VotingInfoDirectVoterExtension on VotingInfoDirectVoter { + VotingInfoDirectVoter copyWith({String? type, String? votingKey}) { + return VotingInfoDirectVoter( + type: type ?? this.type, votingKey: votingKey ?? this.votingKey); + } + + VotingInfoDirectVoter copyWithWrapped( + {Wrapped? type, Wrapped? votingKey}) { + return VotingInfoDirectVoter( + type: (type != null ? type.value : this.type), + votingKey: (votingKey != null ? votingKey.value : this.votingKey)); + } +} + String? animalsNullableToJson(enums.Animals? animals) { return animals?.value; } diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart index b46525fd534..b75d1a4204f 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/generated/catalyst_gateway/cat_gateway_api.models.swagger.g.dart @@ -49,6 +49,27 @@ Map _$DelegationToJson(Delegation instance) => 'power': instance.power, }; +Delegations _$DelegationsFromJson(Map json) => Delegations( + delegations: (json['delegations'] as List?) + ?.map((e) => Delegation.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$DelegationsToJson(Delegations instance) => + { + 'delegations': instance.delegations.map((e) => e.toJson()).toList(), + }; + +DirectVoter _$DirectVoterFromJson(Map json) => DirectVoter( + votingKey: json['voting_key'] as String, + ); + +Map _$DirectVoterToJson(DirectVoter instance) => + { + 'voting_key': instance.votingKey, + }; + FragmentStatus _$FragmentStatusFromJson(Map json) => FragmentStatus(); @@ -103,7 +124,8 @@ RegistrationInfo _$RegistrationInfoFromJson(Map json) => rewardsAddress: json['rewards_address'] as String, txHash: json['tx_hash'] as String, nonce: json['nonce'] as int, - votingInfo: json['voting_info'] as Map, + votingInfo: + VotingInfo.fromJson(json['voting_info'] as Map), ); Map _$RegistrationInfoToJson(RegistrationInfo instance) => @@ -111,7 +133,7 @@ Map _$RegistrationInfoToJson(RegistrationInfo instance) => 'rewards_address': instance.rewardsAddress, 'tx_hash': instance.txHash, 'nonce': instance.nonce, - 'voting_info': instance.votingInfo, + 'voting_info': instance.votingInfo.toJson(), }; RejectedFragment _$RejectedFragmentFromJson(Map json) => @@ -269,3 +291,39 @@ Map _$VoterRegistrationToJson(VoterRegistration instance) => 'last_updated': instance.lastUpdated.toIso8601String(), 'final': instance.$final, }; + +VotingInfo _$VotingInfoFromJson(Map json) => VotingInfo(); + +Map _$VotingInfoToJson(VotingInfo instance) => + {}; + +VotingInfoDelegations _$VotingInfoDelegationsFromJson( + Map json) => + VotingInfoDelegations( + type: json['type'] as String, + delegations: (json['delegations'] as List?) + ?.map((e) => Delegation.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$VotingInfoDelegationsToJson( + VotingInfoDelegations instance) => + { + 'type': instance.type, + 'delegations': instance.delegations.map((e) => e.toJson()).toList(), + }; + +VotingInfoDirectVoter _$VotingInfoDirectVoterFromJson( + Map json) => + VotingInfoDirectVoter( + type: json['type'] as String, + votingKey: json['voting_key'] as String, + ); + +Map _$VotingInfoDirectVoterToJson( + VotingInfoDirectVoter instance) => + { + 'type': instance.type, + 'voting_key': instance.votingKey, + };