refactor carefully
Summary
Stellar address validation is currently implemented three separate times across three different files, each using a slightly different approach. One uses a manual string check, one uses the Stellar SDK's built-in validator, and one has two implementations in the same file where only one is actually called. This inconsistency means the same address could be accepted by one part of the system and rejected by another. This issue consolidates all three into a single shared validator so there is one implementation, one place to update, and one set of tests.
Background: The three diverging implementations
Across the codebase, Stellar address validation currently looks like this in three different places:
ClaimRedemptionProvider checks manually that the address starts with 'G' and is exactly 56 characters long. This is a rough approximation: it would accept many strings that are not valid Stellar keys.
ValidationProvider uses StrKey.isValidEd25519PublicKey() from the Stellar SDK, which is the correct, cryptographically aware validation method. However the same file also has a separate private method using a regex that is never called - dead code that will confuse contributors.
RedeemClaimDto uses a @matches regex decorator from class-validator. This catches invalid addresses at the API boundary, which is good, but the regex used (^G[A-Z0-9]{55}$) is subtly different from both of the above - Stellar addresses use Base32 encoding which includes digits 2-7, not 0-9. The regex technically over-accepts some invalid characters.
Why StrKey.isValidEd25519PublicKey() is the right answer
The Stellar SDK's StrKey utility performs checksum validation on top of format validation. A string can match ^G[A-Z0-9]{55}$ and still be an invalid Stellar address because the last few characters encode a checksum. StrKey.isValidEd25519PublicKey() catches these cases. Any shared utility should be built on top of this method.
Where the shared utility should live
src/common/ is the right location for cross-module utilities in this project. Look at how other shared utilities are structured there. The utility should be a simple function or injectable service that takes an address string and either returns a boolean or throws a BadRequestException, expose both forms since different callers need different behaviors (some want to throw, some want to check silently).
What to do with the existing implementations
Once the shared utility exists, remove the private validation methods from ClaimRedemptionProvider and ValidationProvider and replace their calls with the shared utility. The dead regex method in ValidationProvider should be deleted outright. The @matches decorator in RedeemClaimDto can stay as a first-line API boundary check but its regex should be updated to accurately reflect the Base32 character set (^G[A-Z2-7][A-Z2-7]{54}$ is closer, though SDK validation remains the authoritative check).
Where to look
- src/modules/claims/providers/claim-redemption.provider.ts: validateStellarAddress() private method
- src/modules/sweeps/providers/validation.provider.ts: both validateStellarAddress() and isValidStellarAddress() private methods
- src/modules/claims/dto/redeem-claim.dto.ts: the @matches decorator on destinationAddress
- src/common/: where the utility should be created
- Stellar SDK documentation for StrKey.isValidEd25519PublicKey()
Acceptance Criteria
Summary
Stellar address validation is currently implemented three separate times across three different files, each using a slightly different approach. One uses a manual string check, one uses the Stellar SDK's built-in validator, and one has two implementations in the same file where only one is actually called. This inconsistency means the same address could be accepted by one part of the system and rejected by another. This issue consolidates all three into a single shared validator so there is one implementation, one place to update, and one set of tests.
Background: The three diverging implementations
Across the codebase, Stellar address validation currently looks like this in three different places:
ClaimRedemptionProvider checks manually that the address starts with 'G' and is exactly 56 characters long. This is a rough approximation: it would accept many strings that are not valid Stellar keys.
ValidationProvider uses StrKey.isValidEd25519PublicKey() from the Stellar SDK, which is the correct, cryptographically aware validation method. However the same file also has a separate private method using a regex that is never called - dead code that will confuse contributors.
RedeemClaimDto uses a @matches regex decorator from class-validator. This catches invalid addresses at the API boundary, which is good, but the regex used (^G[A-Z0-9]{55}$) is subtly different from both of the above - Stellar addresses use Base32 encoding which includes digits 2-7, not 0-9. The regex technically over-accepts some invalid characters.
Why StrKey.isValidEd25519PublicKey() is the right answer
The Stellar SDK's StrKey utility performs checksum validation on top of format validation. A string can match ^G[A-Z0-9]{55}$ and still be an invalid Stellar address because the last few characters encode a checksum. StrKey.isValidEd25519PublicKey() catches these cases. Any shared utility should be built on top of this method.
Where the shared utility should live
src/common/ is the right location for cross-module utilities in this project. Look at how other shared utilities are structured there. The utility should be a simple function or injectable service that takes an address string and either returns a boolean or throws a BadRequestException, expose both forms since different callers need different behaviors (some want to throw, some want to check silently).
What to do with the existing implementations
Once the shared utility exists, remove the private validation methods from ClaimRedemptionProvider and ValidationProvider and replace their calls with the shared utility. The dead regex method in ValidationProvider should be deleted outright. The @matches decorator in RedeemClaimDto can stay as a first-line API boundary check but its regex should be updated to accurately reflect the Base32 character set (^G[A-Z2-7][A-Z2-7]{54}$ is closer, though SDK validation remains the authoritative check).
Where to look
Acceptance Criteria