Repository navigation
fix: normalize case and whitespace in Go and Dart muxed encoders #346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,16 +1,66 @@ | ||||||
| import 'dart:typed_data'; | ||||||
| import '../util/strkey.dart'; | ||||||
| import '../util/strey.ydart'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Fix the import path. The file does not compile. Line 2 imports 🐛 Proposed fix--- "a/packages/core-dart/lib/src/muxed/encode.dart"
+++ "b/packages/core-dart/lib/src/muxed/encode.dart"
@@ -1,5 +1,5 @@
import 'dart:typed_data';
-import '../util/strey.ydart';
+import '../util/strkey.dart';
class MuxedEncodeError extends Error {
final String message;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| class MuxedEncodeError extends Error { | ||||||
| final String message; | ||||||
| MuxedEncodeError(this.message); | ||||||
| @Override | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Replace Dart annotations are case-sensitive. 🐛 Proposed fix--- "a/packages/core-dart/lib/src/muxed/encode.dart"
+++ "b/packages/core-dart/lib/src/muxed/encode.dart"
@@ -4,7 +4,7 @@
class MuxedEncodeError extends Error {
final String message;
MuxedEncodeError(this.message);
- @Override
+ @override
String toString() => 'MuxedEncodeError: $message';
}
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| String toString() => 'MuxedEncodeError: $message'; | ||||||
| } | ||||||
|
|
||||||
| class MuxedResult { | ||||||
| final String address; | ||||||
| final bool normalized; | ||||||
| final List<String> warnings; | ||||||
|
|
||||||
| MuxedResult(this.address, this.normalized, this.warnings); | ||||||
|
|
||||||
| bool get hasWarnings => warnings.isNotEmpty; | ||||||
| } | ||||||
|
|
||||||
| class MuxedEncoder { | ||||||
| static String encodeMuxed(List<int> ed25519, BigInt id) { | ||||||
| if (ed25519.length != 32) { | ||||||
| static const String nonCanonicalAddressWarning = 'NON_CANONICAL_ADDRESS'; | ||||||
|
|
||||||
| /// Encodes an ed25519 public key and 64-bit ID into a muxed address. | ||||||
| /// | ||||||
| /// The [ed25519] argument may be provided as a 32-byte array, or as a | ||||||
| /// base G-address string. When a string is provided, leading/trailing | ||||||
| /// whitespace is trimmed and lowercase Base32 characters are uppercased. | ||||||
| /// If normalization occurred, a [MuxedResult] is returned with | ||||||
| /// `normalized = true` and the `NON_CANONICAL_ADDRESS` warning. | ||||||
| /// | ||||||
| /// When [edTransformed] is false, the returned string is the encoded | ||||||
| /// muxed address. When true, the returned value is a [MuxedResult]. | ||||||
| static Object encodeMuxed(Object ed25519, BigInt id, {bool edTransformed = false}) { | ||||||
| List<String> warnings = <String>[]; | ||||||
| bool normalized = false; | ||||||
| List<int> keyBytes; | ||||||
|
|
||||||
| if (ed25519 is List<int>) { | ||||||
| keyBytes = ed25519; | ||||||
| } else if (ed25519 is Uint8List) { | ||||||
| keyBytes = ed25519; | ||||||
| } else if (ed25519 is String) { | ||||||
| final raw = ed25519; | ||||||
| final trimmed = raw.trim(); | ||||||
| final upper = trimmed.toUpperCase(); | ||||||
| if (trimmed != raw || upper != trimmed) { | ||||||
| normalized = true; | ||||||
| warnings.add(nonCanonicalAddressWarning); | ||||||
| } | ||||||
| keyBytes = StrKeyUtil.decodeEd25519PublicKey(upper); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Replace the call to The supplied
Validate the string with 🐛 Proposed fix--- "a/packages/core-dart/lib/src/muxed/encode.dart"
+++ "b/packages/core-dart/lib/src/muxed/encode.dart"
@@ -48,7 +48,10 @@
normalized = true;
warnings.add(nonCanonicalAddressWarning);
}
- keyBytes = StrKeyUtil.decodeEd25519PublicKey(upper);
+ if (detect(upper) != AddressKind.g) {
+ throw ArgumentError('Invalid base G address');
+ }
+ keyBytes = StrKeyUtil.decodeBase32(upper).sublist(1, 33);
} else {
throw ArgumentError('ED25519 public key must be a 32-byte array or a G-address string');
}🤖 Prompt for AI Agents |
||||||
| } else { | ||||||
| throw ArgumentError('ED25519 public key must be a 32-byte array or a G-address string'); | ||||||
| } | ||||||
|
|
||||||
| if (keyBytes.length != 32) { | ||||||
| throw ArgumentError('ED25519 public key must be 32 bytes'); | ||||||
| } | ||||||
|
|
||||||
| // Payload for Muxed address: version (1) + pubkey (32) + id (8) | ||||||
| final data = Uint8List(1 + 32 + 8); | ||||||
| data[0] = 0x60; // Version M (96) | ||||||
| data.setRange(1, 33, ed25519); | ||||||
| data.setRange(1, 33, keyBytes); | ||||||
|
|
||||||
| // Encode ID as 64-bit big-endian | ||||||
| var remainingId = id; | ||||||
|
|
@@ -28,6 +78,16 @@ class MuxedEncoder { | |||||
| finalData[data.length] = checksum & 0xFF; | ||||||
| finalData[data.length + 1] = (checksum >> 8) & 0xFF; | ||||||
|
|
||||||
| return StrKeyUtil.encodeBase32(finalData); | ||||||
| final encoded = StrKeyUtil.encodeBase32(finalData); | ||||||
|
|
||||||
| if (edTransformed) { | ||||||
| return MuxedResult(encoded, normalized, warnings); | ||||||
| } | ||||||
| return encoded; | ||||||
| } | ||||||
|
|
||||||
| /// Convenience wrapper that always returns a structured result. | ||||||
| static MuxedResult encodeMuxedWithMetadata(Object ed25519, BigInt id) { | ||||||
| return encodeMuxed(ed25519, id, edTransformed: true) as MuxedResult; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,16 +24,38 @@ import 'encode.dart'; | |||||||
| /// [flutter-web-bigint.md](../../../../docs/guides/flutter-web-bigint.md). | ||||||||
| class MuxedAddress { | ||||||||
| static String encode({required String baseG, required BigInt id}) { | ||||||||
| return encodeWithMetadata(baseG: baseG, id: id).result; | ||||||||
| } | ||||||||
|
|
||||||||
| /// Encodes a muxed address and returns normalization metadata. | ||||||||
| /// | ||||||||
| /// If the input required trimming or uppercasing, the result will | ||||||||
| /// include a `NON_CANONICAL_ADDRESS` warning and the normalized base G-address. | ||||||||
| static MuxedEncodeResult encodeWithMetadata({ | ||||||||
| required String baseG, | ||||||||
| required BigInt id, | ||||||||
| }) { | ||||||||
| final uint64Max = BigInt.parse('18446744073709551615'); | ||||||||
| if (id < BigInt.zero || id > uint64Max) { | ||||||||
| throw const StellarAddressException('ID out of uint64 range'); | ||||||||
| } | ||||||||
|
|
||||||||
| if (detect(baseG) != AddressKind.g) { | ||||||||
| final normalized = baseG.trim().toUpperCase(); | ||||||||
| final normalized = normalizedBaseG; | ||||||||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Remove the duplicate Line 43 and Line 44 both declare 🐛 Proposed fix--- "a/packages/core-dart/lib/src/muxed/muxed_address.dart"
+++ "b/packages/core-dart/lib/src/muxed/muxed_address.dart"
@@ -40,9 +40,8 @@
throw const StellarAddressException('ID out of uint64 range');
}
final normalized = baseG.trim().toUpperCase();
- final normalized = normalizedBaseG;
final wasNormalized = normalized != baseG;
if (detect(normalized) != AddressKind.g) {
throw const StellarAddressException('Invalid base G address');📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| final wasNormalized = normalized != baseG; | ||||||||
|
|
||||||||
| if (detect(normalized) != AddressKind.g) { | ||||||||
| throw const StellarAddressException('Invalid base G address'); | ||||||||
| } | ||||||||
|
|
||||||||
| return MuxedEncoder.encodeMuxed(_decodeG(baseG), id); | ||||||||
| final result = MuxedEncoder.encodeMuxed(_decodeG(normalized), id); | ||||||||
| return MuxedEncodeResult( | ||||||||
| result: result, | ||||||||
|
Comment on lines
+51
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,120p' packages/core-dart/lib/src/muxed/muxed_address.dart
sed -n '1,100p' packages/core-dart/lib/src/muxed/encode.dart
rg -n 'encodeMuxed\s*\(' packages/core-dart
rg -n 'sdk:' packages/core-dart/pubspec.yamlRepository: Boxkit-Labs/stellar-address-kit Length of output: 7548 Keep
Suggested fix- static Object encodeMuxed(Object ed25519, BigInt id, {bool edTransformed = false}) {
+ static String encodeMuxed(Object ed25519, BigInt id) {
+ return _encodeMuxedWithMetadata(ed25519, id).address;
+ }
+
+ static MuxedResult encodeMuxedWithMetadata(Object ed25519, BigInt id) {
+ return _encodeMuxedWithMetadata(ed25519, id);
+ }
+
+ static MuxedResult _encodeMuxedWithMetadata(Object ed25519, BigInt id) {
...
- if (edTransformed) {
- return MuxedResult(encoded, normalized, warnings);
- }
- return encoded;
+ return MuxedResult(encoded, normalized, warnings);
}
-
- static MuxedResult encodeMuxedWithMetadata(Object ed25519, BigInt id) {
- return encodeMuxed(ed25519, id, edTransformed: true) as MuxedResult;
- }🤖 Prompt for AI Agents |
||||||||
| normalizedBaseG: wasNormalized ? normalized : null, | ||||||||
| warnings: wasNormalized | ||||||||
| ? const [MuxedNormalizationWarning.nonCanonicalAddress] | ||||||||
| : const [], | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| static DecodedMuxedAddress decode(String mAddress) { | ||||||||
|
|
@@ -54,3 +76,35 @@ class MuxedAddress { | |||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /// Warning emitted when a muxed encode input was normalized. | ||||||||
| enum MuxedNormalizationWarning { | ||||||||
| /// The base G-address had leading/trailing whitespace or non-uppercase | ||||||||
| /// Base32 characters and was normalized before encoding. | ||||||||
| nonCanonicalAddress, | ||||||||
| } | ||||||||
|
|
||||||||
| /// Structured result of a muxed encode operation. | ||||||||
| class MuxedEncodeResult { | ||||||||
| const MuxedEncodeResult({ | ||||||||
| required this.result, | ||||||||
| this.normalizedBaseG, | ||||||||
| this.warnings = const [], | ||||||||
| }); | ||||||||
|
|
||||||||
| /// The encoded M-address. | ||||||||
| final String result; | ||||||||
|
|
||||||||
| /// The normalized base G-address, or `null` if no normalization occurred. | ||||||||
| final String? normalizedBaseG; | ||||||||
|
|
||||||||
| /// Warnings emitted during encoding. | ||||||||
| final List<MuxedNormalizationWarning> warnings; | ||||||||
|
|
||||||||
| /// Whether the input required normalization. | ||||||||
| bool get wasNormalized => normalizedBaseG != null; | ||||||||
|
|
||||||||
| /// Whether a `NON_CANONICAL_ADDRESS` warning was emitted. | ||||||||
| bool get hasNonCanonicalAddressWarning => | ||||||||
| warnings.contains(MuxedNormalizationWarning.nonCanonicalAddress); | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import ( | |
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| ) | ||
| ) | ||
|
|
||
| type WarningCode string | ||
|
|
||
|
|
@@ -15,7 +15,7 @@ const ( | |
| WarnMemoPresentWithMuxed WarningCode = "MEMO_PRESENT_WITH_MUXED" | ||
| WarnContractSenderDetected WarningCode = "CONTRACT_SENDER_DETECTED" | ||
| WarnMemoTextUnroutable WarningCode = "MEMO_TEXT_UNROUTABLE" | ||
| WarnMemoIDInvalidFormat WarningCode = "MEMO_ID_INVALID_FORMAT" | ||
| WarnMemoIDInvalidFormat WarningCode = "MEMO_ID_INVALID_FORMAT" | ||
| WarnUnsupportedMemoType WarningCode = "UNSUPPORTED_MEMO_TYPE" | ||
| WarnInvalidDestination WarningCode = "INVALID_DESTINATION" | ||
| WarnMissingRequiredMemo WarningCode = "MISSING_REQUIRED_MEMO" | ||
|
|
@@ -28,6 +28,7 @@ type Warning struct { | |
| Severity string `json:"severity"` | ||
| Normalization *Normalization `json:"normalization,omitempty"` | ||
| Context *WarningContext `json:"context,omitempty"` | ||
|
|
||
| } | ||
|
|
||
| type Normalization struct { | ||
|
|
@@ -38,6 +39,7 @@ type Normalization struct { | |
| type WarningContext struct { | ||
| DestinationKind string `json:"destinationKind,omitempty"` | ||
| MemoType string `json:"memoType,omitempty"` | ||
|
|
||
| } | ||
|
|
||
| type warningJSON struct { | ||
|
|
@@ -46,14 +48,16 @@ type warningJSON struct { | |
| Severity string `json:"severity"` | ||
| Normalization *Normalization `json:"normalization,omitempty"` | ||
| Context *warningContextJSON `json:"context,omitempty"` | ||
|
|
||
| } | ||
|
|
||
| type warningContextJSON struct { | ||
| DestinationKind string `json:"destinationKind,omitempty"` | ||
| MemoType string `json:"memoType,omitempty"` | ||
|
|
||
| } | ||
|
|
||
| func (w Warning) MarshalJSON() ([]byte, error) { | ||
| func (w Warning) MarshalJSON() ([ byte, error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Correct the
🧰 Tools🪛 golangci-lint (2.14.0)[error] 60-60: unexpected comma; expecting ] (typecheck) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| payload := warningJSON{ | ||
| Code: w.Code, | ||
| Message: w.Message, | ||
|
|
@@ -165,7 +169,7 @@ func (w Warning) validate(raw map[string]json.RawMessage) error { | |
| return err | ||
| } | ||
| if w.normalizedContext() != nil || len(bytes.TrimSpace(raw["context"])) != 0 { | ||
| return fmt.Errorf("warning %q does not allow context", w.Code) | ||
| return fmt.Errorf("warning %q: does not allow context", w.Code) | ||
| } | ||
| return nil | ||
| } | ||
|
|
@@ -187,7 +191,7 @@ func (w Warning) validate(raw map[string]json.RawMessage) error { | |
| return fmt.Errorf("warning %q does not allow normalization", w.Code) | ||
| } | ||
| if w.normalizedContext() != nil || len(bytes.TrimSpace(raw["context"])) != 0 { | ||
| return fmt.Errorf("warning %q does not allow context", w.Code) | ||
| return fmt.Errorf("warning %q: does not allow context", w.Code) | ||
| } | ||
|
|
||
| return nil | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,54 +1,54 @@ | ||
| package muxed | ||
|
|
||
| import ( | ||
| "encoding/binary" | ||
| "encoding/binary" | ||
|
|
||
| "github.com/Boxkit-Labs/stellar-address-kit/packages/core-go/address" | ||
| "github.com/Boxkit-Labs/stellar-address-kit/packages/core-go/address" | ||
| ) | ||
|
|
||
| // DecodeMuxed extracts the base G-address and uint64 ID from a muxed | ||
| // M-address. The 64-bit ID is parsed directly from the big-endian payload | ||
| // using binary.BigEndian.Uint64, matching the Stellar protocol's native | ||
| // byte-aligned serialization. | ||
| func DecodeMuxed(mAddress string) (string, uint64, error) { | ||
| versionByte, payload, err := address.DecodeStrKey(mAddress) | ||
| if err != nil { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidGAddress, | ||
| Input: mAddress, | ||
| Message: "failed to decode muxed address", | ||
| Cause: err, | ||
| } | ||
| } | ||
|
|
||
| if versionByte != address.VersionByteMuxedAccount { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrUnknownVersionByte, | ||
| Input: mAddress, | ||
| Message: "expected muxed account version byte", | ||
| } | ||
| } | ||
|
|
||
| if len(payload) != 40 { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidLength, | ||
| Input: mAddress, | ||
| Message: "muxed payload must be 40 bytes", | ||
| } | ||
| } | ||
|
|
||
| pubkey := payload[:32] | ||
| id := binary.BigEndian.Uint64(payload[32:40]) | ||
|
|
||
| baseG, err := address.EncodeStrKey(address.VersionByteAccountID, pubkey) | ||
| if err != nil { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidGAddress, | ||
| Input: mAddress, | ||
| Message: "failed to encode base G-address", | ||
| Cause: err, | ||
| } | ||
| } | ||
|
|
||
| return baseG, id, nil | ||
| versionByte, payload, err := address.DecodeStrKey(mAddress) | ||
| if err != nil { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidGAddress, | ||
| Input: mAddress, | ||
| Message: "failed to decode muxed address", | ||
| Cause: err, | ||
| } | ||
| } | ||
|
|
||
| if versionByte != address.VersionByteMuxedAccount { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrUnknownVersionByte, | ||
| Input: mAddress, | ||
| Message: "expected muxed account version byte", | ||
| } | ||
| } | ||
|
|
||
| if len(payload) != 40 { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidLength, | ||
| Input: mAddress, | ||
| Message: "muxed payload must be 40 bytes", | ||
| } | ||
| } | ||
|
|
||
| pubkey := payload[:32] | ||
| id := binary.BigEndian.Uint64(payload[32:40]) | ||
|
|
||
| baseG, err := address.EncodeStrKey(address.VersionByteAccountID, pubkey) | ||
| if err != nil { | ||
| return "", 0, &AddressError{ | ||
| Code: ErrInvalidGAddress, | ||
| Input: mAddress, | ||
| Message: "failed to encode base G-address", | ||
| Cause: err, | ||
| } | ||
| } | ||
|
|
||
| return baseG, id, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Fix the import path. The file does not compile.
Line 2 imports
../util/strey.key.dart. That file does not exist. TheStrKeyUtilsource is../util/strkey.dart. The change breaksMuxedDecoderand every file that imports it.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents