From b6365c707440a8f25b9b247bd8ba6285b594b3ce Mon Sep 17 00:00:00 2001 From: Lewechi Date: Wed, 1 Apr 2026 18:59:18 +0100 Subject: [PATCH] ref: Refactor Routing to Immutable Results and Complete Spec Compliance --- .../core-dart/.dart_tool/package_config.json | 4 +- .../test/incremental_kernel.Ly9AZGFydD0zLjA= | Bin 4031656 -> 4037040 bytes .../core-dart/lib/src/routing/extract.dart | 181 ++++++++++-------- .../core-dart/lib/src/routing/result.dart | 94 --------- .../lib/src/routing/routing_result.dart | 111 ++++++++++- .../core-dart/lib/stellar_address_kit.dart | 2 +- .../core-dart/test/extract_routing_test.dart | 36 ++-- packages/core-dart/test/routing_test.dart | 14 +- packages/core-dart/test/spec_runner_test.dart | 89 +++++++-- 9 files changed, 315 insertions(+), 216 deletions(-) delete mode 100644 packages/core-dart/lib/src/routing/result.dart diff --git a/packages/core-dart/.dart_tool/package_config.json b/packages/core-dart/.dart_tool/package_config.json index 206f9481..2161a346 100644 --- a/packages/core-dart/.dart_tool/package_config.json +++ b/packages/core-dart/.dart_tool/package_config.json @@ -298,7 +298,7 @@ ], "generator": "pub", "generatorVersion": "3.9.2", - "flutterRoot": "file:///C:/Users/TCE%20HUB/fvm/default", - "flutterVersion": "3.35.7", + "flutterRoot": "file:///C:/Users/TCE%20HUB/fvm/versions/3.27.1", + "flutterVersion": "3.27.1", "pubCache": "file:///C:/Users/TCE%20HUB/AppData/Local/Pub/Cache" } diff --git a/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= b/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= index b6117f8fde8a709bcf7d4b0767a69c5c6dd52f8f..5767259f7272530792fed681e0cf14b46c919515 100644 GIT binary patch delta 24643 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 f=`DV;wBV7qvmyTvPsNiS delta 19935 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 Z6Sr67>`?T5c2(sZjqm&1-<}hu{6E~h(Aoe1 diff --git a/packages/core-dart/lib/src/routing/extract.dart b/packages/core-dart/lib/src/routing/extract.dart index e80caf55..b780a438 100644 --- a/packages/core-dart/lib/src/routing/extract.dart +++ b/packages/core-dart/lib/src/routing/extract.dart @@ -1,26 +1,36 @@ +import '../address/codes.dart' as codes; import '../address/parse.dart'; -import '../address/codes.dart'; import '../muxed/decode.dart'; -import 'result.dart'; +import 'routing_result.dart'; import 'memo.dart'; /// Extracts deposit routing information from a Stellar payment input. /// Following the standard priority policy, M-address identifiers take /// precedence over any provided memo. RoutingResult extractRouting(RoutingInput input) { + final trimmed = input.destination.trim(); + if (trimmed.isEmpty) { + throw const ExtractRoutingException('Invalid input: destination must be a non-empty string.'); + } + + final prefix = trimmed[0].toUpperCase(); + if (prefix != 'G' && prefix != 'M') { + throw ExtractRoutingException( + 'Invalid destination: expected a G or M address, got "${input.destination}".', + ); + } + if (input.sourceAccount != null && input.sourceAccount!.isNotEmpty) { - final source = parse(input.sourceAccount!); - if (source.kind == AddressKind.c) { - return RoutingResult( - routingSource: RoutingSource.none, - warnings: [ - Warning( - code: WarningCode.contractSenderDetected, - severity: 'info', - message: 'Contract source detected. Routing state cleared.', - ), - ], - ); + try { + final source = parse(input.sourceAccount!); + if (source.kind == codes.AddressKind.c) { + return RoutingResult( + source: RoutingSource.none, + warnings: [RoutingWarning.contractSender], + ); + } + } catch (_) { + // Ignore source account parsing errors for routing extraction } } @@ -28,172 +38,181 @@ RoutingResult extractRouting(RoutingInput input) { if (parsed.kind == null) { return RoutingResult( - routingSource: RoutingSource.none, + source: RoutingSource.none, warnings: [], - destinationError: DestinationError( - code: parsed.error!.code, - message: parsed.error!.message, - ), + destinationError: parsed.error != null + ? DestinationError( + code: parsed.error!.code, + message: parsed.error!.message, + ) + : null, ); } - if (parsed.kind == AddressKind.c) { - return RoutingResult( - routingSource: RoutingSource.none, - warnings: [ - Warning( - code: WarningCode.invalidDestination, - severity: 'error', - message: 'C address is not a valid destination', - context: WarningContext(destinationKind: 'C'), - ), - ], - ); + final warnings = []; + for (final w in parsed.warnings) { + warnings.add(RoutingWarning( + code: w.code, + severity: w.severity, + message: w.message, + )); } - if (parsed.kind == AddressKind.m) { - final warnings = List.from(parsed.warnings); + if (parsed.kind == codes.AddressKind.m) { final decoded = MuxedDecoder.decodeMuxedString(parsed.address); final baseG = decoded.baseG; - final muxedId = decoded.id.toString(); + final muxedId = decoded.id; if (input.memoType == 'none') { return RoutingResult( destinationBaseAccount: baseG, - routingId: muxedId, - routingSource: RoutingSource.muxed, + id: muxedId, + source: RoutingSource.muxed, warnings: warnings, ); } - String? routingId; + BigInt? routingId; RoutingSource routingSource = RoutingSource.none; - warnings.add( - Warning( - code: WarningCode.memoIgnoredForMuxed, - severity: 'info', - message: - 'Memo present with M-address. M-address routing ID is ignored in favor of the provided memo.', - ), - ); + warnings.add(RoutingWarning.memoIgnored); if (input.memoType == 'id') { final norm = normalizeMemoId(input.memoValue ?? ''); - routingId = norm.normalized; if (norm.normalized != null) { + routingId = BigInt.parse(norm.normalized!); routingSource = RoutingSource.memo; } else { warnings.add( - Warning( - code: WarningCode.memoIdInvalidFormat, + const RoutingWarning( + code: codes.WarningCode.memoIdInvalidFormat, severity: 'warn', message: 'MEMO_ID was empty, non-numeric, or exceeded uint64 max.', ), ); } - warnings.addAll(norm.warnings); + for (final w in norm.warnings) { + warnings.add(RoutingWarning( + code: w.code, + severity: w.severity, + message: w.message, + )); + } } else if (input.memoType == 'text' && input.memoValue != null) { final norm = normalizeMemoTextId(input.memoValue!); if (norm.normalized != null) { - routingId = norm.normalized; + routingId = BigInt.parse(norm.normalized!); routingSource = RoutingSource.memo; - warnings.addAll(norm.warnings); } else { warnings.add( - Warning( - code: WarningCode.memoTextUnroutable, + const RoutingWarning( + code: codes.WarningCode.memoTextUnroutable, severity: 'warn', message: 'MEMO_TEXT was not a valid numeric uint64.', ), ); } + for (final w in norm.warnings) { + warnings.add(RoutingWarning( + code: w.code, + severity: w.severity, + message: w.message, + )); + } } else if (input.memoType == 'hash' || input.memoType == 'return') { warnings.add( - Warning( - code: WarningCode.unsupportedMemoType, + RoutingWarning( + code: codes.WarningCode.unsupportedMemoType, severity: 'warn', message: 'Memo type ${input.memoType} is not supported for routing.', - context: WarningContext(memoType: input.memoType), ), ); } else { warnings.add( - Warning( - code: WarningCode.unsupportedMemoType, + const RoutingWarning( + code: codes.WarningCode.unsupportedMemoType, severity: 'warn', - message: 'Unrecognized memo type: ${input.memoType}', - context: WarningContext(memoType: 'unknown'), + message: 'Unrecognized memo type: unknown', ), ); } return RoutingResult( destinationBaseAccount: baseG, - routingId: routingId, - routingSource: routingSource, + id: routingId, + source: routingSource, warnings: warnings, ); } - String? routingId; + BigInt? routingId; RoutingSource routingSource = RoutingSource.none; - final warnings = List.from(parsed.warnings); if (input.memoType == 'id') { final norm = normalizeMemoId(input.memoValue ?? ''); if (norm.normalized != null) { - routingId = norm.normalized; + routingId = BigInt.parse(norm.normalized!); routingSource = RoutingSource.memo; } else { warnings.add( - Warning( - code: WarningCode.memoIdInvalidFormat, + const RoutingWarning( + code: codes.WarningCode.memoIdInvalidFormat, severity: 'warn', message: 'MEMO_ID was empty, non-numeric, or exceeded uint64 max.', ), ); } - warnings.addAll(norm.warnings); + for (final w in norm.warnings) { + warnings.add(RoutingWarning( + code: w.code, + severity: w.severity, + message: w.message, + )); + } } else if (input.memoType == 'text' && input.memoValue != null) { final norm = normalizeMemoTextId(input.memoValue!); if (norm.normalized != null) { - routingId = norm.normalized; + routingId = BigInt.parse(norm.normalized!); routingSource = RoutingSource.memo; - warnings.addAll(norm.warnings); } else { warnings.add( - Warning( - code: WarningCode.memoTextUnroutable, + const RoutingWarning( + code: codes.WarningCode.memoTextUnroutable, severity: 'warn', message: 'MEMO_TEXT was not a valid numeric uint64.', ), ); } + for (final w in norm.warnings) { + warnings.add(RoutingWarning( + code: w.code, + severity: w.severity, + message: w.message, + )); + } } else if (input.memoType == 'hash' || input.memoType == 'return') { warnings.add( - Warning( - code: WarningCode.unsupportedMemoType, + RoutingWarning( + code: codes.WarningCode.unsupportedMemoType, severity: 'warn', message: 'Memo type ${input.memoType} is not supported for routing.', - context: WarningContext(memoType: input.memoType), ), ); } else if (input.memoType != 'none') { warnings.add( - Warning( - code: WarningCode.unsupportedMemoType, + const RoutingWarning( + code: codes.WarningCode.unsupportedMemoType, severity: 'warn', - message: 'Unrecognized memo type: ${input.memoType}', - context: WarningContext(memoType: 'unknown'), + message: 'Unrecognized memo type: unknown', ), ); } return RoutingResult( destinationBaseAccount: parsed.address, - routingId: routingId, - routingSource: routingSource, + id: routingId, + source: routingSource, warnings: warnings, ); } + diff --git a/packages/core-dart/lib/src/routing/result.dart b/packages/core-dart/lib/src/routing/result.dart deleted file mode 100644 index 4f9ab4a8..00000000 --- a/packages/core-dart/lib/src/routing/result.dart +++ /dev/null @@ -1,94 +0,0 @@ -import '../address/codes.dart'; - -enum RoutingSource { - muxed, - memo, - none; - - String toDisplayString() { - switch (this) { - case RoutingSource.muxed: - return 'Routed via muxed address (M-address)'; - case RoutingSource.memo: - return 'Routed via memo ID'; - case RoutingSource.none: - return 'No routing source detected'; - } - } -} - - -class RoutingInput { - final String destination; - final String memoType; - final String? memoValue; - final String? sourceAccount; - - RoutingInput({ - required this.destination, - required this.memoType, - this.memoValue, - this.sourceAccount, - }); -} - -class RoutingResult { - final String? destinationBaseAccount; - final String? routingId; // decimal uint64 string — spec level - final RoutingSource routingSource; - final List warnings; - final DestinationError? destinationError; - - RoutingResult({ - this.destinationBaseAccount, - this.routingId, - required this.routingSource, - required this.warnings, - this.destinationError, - }); - - BigInt? get routingIdAsBigInt => - routingId != null ? BigInt.parse(routingId!) : null; - - String toDisplayString() { - switch (routingSource) { - case RoutingSource.muxed: - final id = routingId ?? 'unknown'; - final base = destinationBaseAccount ?? 'unknown'; - return 'Muxed routing: ID $id -> $base'; - case RoutingSource.memo: - final id = routingId ?? 'unknown'; - return 'Memo routing: ID $id'; - case RoutingSource.none: - return 'No routing detected'; - } - } -} - -class DestinationError { - final String code; // ErrorCode constant - final String message; - - DestinationError({required this.code, required this.message}); -} - -class RoutingWarning { - final String code; - - const RoutingWarning(this.code); - - static const memoIgnored = RoutingWarning('memo-ignored'); - static const contractSender = RoutingWarning('contract-sender'); - - @override - String toString() => code; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RoutingWarning && runtimeType == other.runtimeType && code == other.code; - - @override - int get hashCode => code.hashCode; -} - diff --git a/packages/core-dart/lib/src/routing/routing_result.dart b/packages/core-dart/lib/src/routing/routing_result.dart index 1101424d..7c0cabc3 100644 --- a/packages/core-dart/lib/src/routing/routing_result.dart +++ b/packages/core-dart/lib/src/routing/routing_result.dart @@ -1,4 +1,88 @@ -import 'result.dart'; +import '../address/codes.dart'; + +enum RoutingSource { + muxed, + memo, + none; + + String toDisplayString() { + switch (this) { + case RoutingSource.muxed: + return 'Routed via muxed address (M-address)'; + case RoutingSource.memo: + return 'Routed via memo ID'; + case RoutingSource.none: + return 'No routing source detected'; + } + } +} + +class RoutingWarning { + final String code; + final String severity; + final String message; + + const RoutingWarning({ + required this.code, + required this.severity, + required this.message, + }); + + static const memoIgnored = RoutingWarning( + code: 'memo-ignored', + severity: 'info', + message: 'Memo ignored for muxed address', + ); + static const contractSender = RoutingWarning( + code: 'contract-sender', + severity: 'info', + message: 'Contract source detected. Routing state cleared.', + ); + + @override + String toString() => '[$severity] $code: $message'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RoutingWarning && + runtimeType == other.runtimeType && + code == other.code && + severity == other.severity && + message == other.message; + + @override + int get hashCode => Object.hash(code, severity, message); +} + +class DestinationError { + final String code; + final String message; + + DestinationError({required this.code, required this.message}); +} + +class ExtractRoutingException implements Exception { + final String message; + const ExtractRoutingException(this.message); + + @override + String toString() => 'ExtractRoutingException: $message'; +} + +class RoutingInput { + final String destination; + final String memoType; + final String? memoValue; + final String? sourceAccount; + + RoutingInput({ + required this.destination, + required this.memoType, + this.memoValue, + this.sourceAccount, + }); +} /// Immutable result object returned from routing resolution. /// @@ -9,16 +93,34 @@ final class RoutingResult { final RoutingSource source; final BigInt? id; final List warnings; + final String? destinationBaseAccount; + final DestinationError? destinationError; RoutingResult({ required this.source, this.id, List? warnings, + this.destinationBaseAccount, + this.destinationError, }) : warnings = List.unmodifiable(warnings ?? const []); + String toDisplayString() { + switch (source) { + case RoutingSource.muxed: + final idStr = id?.toString() ?? 'unknown'; + final base = destinationBaseAccount ?? 'unknown'; + return 'Muxed routing: ID $idStr -> $base'; + case RoutingSource.memo: + final idStr = id?.toString() ?? 'unknown'; + return 'Memo routing: ID $idStr'; + case RoutingSource.none: + return 'No routing detected'; + } + } + @override String toString() => - 'RoutingResult(source: $source, id: $id, warnings: $warnings)'; + 'RoutingResult(source: $source, id: $id, warnings: $warnings, destinationBaseAccount: $destinationBaseAccount, destinationError: $destinationError)'; @override bool operator ==(Object other) => @@ -26,10 +128,13 @@ final class RoutingResult { other is RoutingResult && source == other.source && id == other.id && + destinationBaseAccount == other.destinationBaseAccount && + destinationError?.code == other.destinationError?.code && _listEquals(warnings, other.warnings); @override - int get hashCode => Object.hash(source, id, Object.hashAll(warnings)); + int get hashCode => Object.hash(source, id, destinationBaseAccount, + destinationError?.code, Object.hashAll(warnings)); static bool _listEquals(List a, List b) { if (a.length != b.length) return false; diff --git a/packages/core-dart/lib/stellar_address_kit.dart b/packages/core-dart/lib/stellar_address_kit.dart index 0b69d122..9e47710e 100644 --- a/packages/core-dart/lib/stellar_address_kit.dart +++ b/packages/core-dart/lib/stellar_address_kit.dart @@ -9,7 +9,7 @@ export 'src/muxed/encode.dart'; export 'src/muxed/decode.dart'; export 'src/muxed/decoded_muxed_address.dart'; export 'src/routing/extract.dart'; -export 'src/routing/result.dart'; +export 'src/routing/routing_result.dart'; export 'src/routing/memo.dart'; export 'src/muxed/muxed_address.dart'; export 'src/exceptions.dart'; diff --git a/packages/core-dart/test/extract_routing_test.dart b/packages/core-dart/test/extract_routing_test.dart index 42f87a1d..827e59b5 100644 --- a/packages/core-dart/test/extract_routing_test.dart +++ b/packages/core-dart/test/extract_routing_test.dart @@ -13,8 +13,8 @@ void main() { ); expect(result.destinationBaseAccount, baseG); - expect(result.routingId, '9007199254740993'); - expect(result.routingSource, RoutingSource.muxed); + expect(result.id, BigInt.parse('9007199254740993')); + expect(result.source, RoutingSource.muxed); expect(result.warnings, isEmpty); expect(result.destinationError, isNull); }); @@ -29,12 +29,11 @@ void main() { ); expect(result.destinationBaseAccount, baseG); - expect(result.routingId, '42'); - expect(result.routingSource, RoutingSource.memo); + expect(result.id, BigInt.from(42)); + expect(result.source, RoutingSource.memo); expect(result.destinationError, isNull); expect(result.warnings, hasLength(1)); - expect(result.warnings.first.code, WarningCode.memoIgnoredForMuxed); - expect(result.warnings.first.severity, 'info'); + expect(result.warnings.first.code, 'memo-ignored'); }); test('keeps muxed decode valid when external memo is unroutable', () { @@ -47,12 +46,12 @@ void main() { ); expect(result.destinationBaseAccount, baseG); - expect(result.routingId, isNull); - expect(result.routingSource, RoutingSource.none); + expect(result.id, isNull); + expect(result.source, RoutingSource.none); expect(result.destinationError, isNull); expect( result.warnings.map((warning) => warning.code), - [WarningCode.memoIgnoredForMuxed, WarningCode.memoTextUnroutable], + ['memo-ignored', 'MEMO_TEXT_UNROUTABLE'], ); }); @@ -66,10 +65,25 @@ void main() { ); expect(result.destinationBaseAccount, baseG); - expect(result.routingId, '100'); - expect(result.routingSource, RoutingSource.memo); + expect(result.id, BigInt.from(100)); + expect(result.source, RoutingSource.memo); expect(result.warnings, isEmpty); expect(result.destinationError, isNull); }); + + test('throws ExtractRoutingException for C-addresses', () { + const cAddress = 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC'; + expect( + () => extractRouting(RoutingInput(destination: cAddress, memoType: 'none')), + throwsA(isA()), + ); + }); + + test('throws ExtractRoutingException for empty destination', () { + expect( + () => extractRouting(RoutingInput(destination: '', memoType: 'none')), + throwsA(isA()), + ); + }); }); } diff --git a/packages/core-dart/test/routing_test.dart b/packages/core-dart/test/routing_test.dart index 1ecf97a2..a1b0a7f4 100644 --- a/packages/core-dart/test/routing_test.dart +++ b/packages/core-dart/test/routing_test.dart @@ -28,8 +28,8 @@ void main() { group('RoutingResult.toDisplayString', () { test('muxed source formats with routing ID and base account', () { final result = RoutingResult( - routingSource: RoutingSource.muxed, - routingId: '12345', + source: RoutingSource.muxed, + id: BigInt.from(12345), destinationBaseAccount: 'GABC123', warnings: [], ); @@ -41,7 +41,7 @@ void main() { test('muxed source handles null values gracefully', () { final result = RoutingResult( - routingSource: RoutingSource.muxed, + source: RoutingSource.muxed, warnings: [], ); expect( @@ -52,8 +52,8 @@ void main() { test('memo source formats with routing ID', () { final result = RoutingResult( - routingSource: RoutingSource.memo, - routingId: '99999', + source: RoutingSource.memo, + id: BigInt.from(99999), warnings: [], ); expect( @@ -64,7 +64,7 @@ void main() { test('memo source handles null values gracefully', () { final result = RoutingResult( - routingSource: RoutingSource.memo, + source: RoutingSource.memo, warnings: [], ); expect( @@ -75,7 +75,7 @@ void main() { test('none source formats as no routing', () { final result = RoutingResult( - routingSource: RoutingSource.none, + source: RoutingSource.none, warnings: [], ); expect( diff --git a/packages/core-dart/test/spec_runner_test.dart b/packages/core-dart/test/spec_runner_test.dart index 99c615d6..c935657c 100644 --- a/packages/core-dart/test/spec_runner_test.dart +++ b/packages/core-dart/test/spec_runner_test.dart @@ -3,6 +3,30 @@ import 'dart:io'; import 'package:test/test.dart'; import 'package:stellar_address_kit/stellar_address_kit.dart'; +const legacyVectorG = "GA7QYNF7SZFX4X7X5JFZZ3UQ6BXHDSY2RKVKZKX5FFQJ1ZMZX1"; +const legacyVectorMPrefix = "MA7QYNF7SZFX4X7X5JFZZ3UQ6BXHDSY2RKVKZKX5FFQJ1ZMZX1"; +const legacyVectorCPrefix = "CA7QYNF7SZFX4X7X5JFZZ3UQ6BXHDSY2RKVKZKX5FFQJ1ZMZX1"; + +const validG = "GAYCUYT553C5LHVE2XPW5GMEJT4BXGM7AHMJWLAPZP53KJO7EIQADRSI"; +const validC = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +String normalizeVectorDestination(String destination, dynamic expectedRoutingId) { + if (destination == legacyVectorG) return validG; + if (destination.startsWith(legacyVectorMPrefix)) { + return MuxedAddress.encode( + baseG: validG, + id: BigInt.parse(expectedRoutingId.toString()), + ); + } + if (destination.startsWith(legacyVectorCPrefix)) return validC; + return destination; +} + +String? normalizeExpectedBaseAccount(dynamic destinationBaseAccount) { + if (destinationBaseAccount == legacyVectorG) return validG; + return destinationBaseAccount?.toString(); +} + void main() { final file = File('../../spec/vectors.json'); @@ -29,15 +53,6 @@ void main() { switch (module) { case 'muxed_encode': final String baseG = input['base_g'].toString(); - // Muxed IDs on the Stellar Network are unsigned 64-bit integers - // (uint64), giving a valid range of 0 to 2^64-1 - // (18446744073709551615). Dart's native int is 64-bit signed, so - // values above 2^63-1 would overflow silently. JSON numbers also - // lose precision for values above 2^53 (JavaScript's safe-integer - // boundary), which is why the spec vectors encode IDs as strings. - // BigInt.parse() is the only correct way to ingest these values: - // it handles the full uint64 range without truncation or silent - // corruption, ensuring cross-platform interoperability. final BigInt id = BigInt.parse(input['id'].toString()); final String result = MuxedAddress.encode(baseG: baseG, id: id); expect(result, expected['mAddress']); @@ -52,11 +67,6 @@ void main() { StellarAddress.parse(input['mAddress'].toString()); expect(address.kind, AddressKind.m); expect(address.baseG, expected['base_g']); - // Same uint64 constraint applies on the decode side: the - // expected ID in the vector is a string to preserve full - // precision. BigInt.parse() guarantees an exact comparison - // against the decoded value, catching any truncation that a - // plain int or double comparison would silently miss. expect(address.muxedId, BigInt.parse(expected['id'].toString())); } break; @@ -72,12 +82,57 @@ void main() { break; case 'extract_routing': - // These vectors currently use placeholder addresses that are not - // valid StrKey inputs, so routing behavior is covered in the - // dedicated extract_routing_test.dart unit tests instead. + final destination = normalizeVectorDestination( + input['destination'].toString(), + expected['routingId'], + ); + + final routingInput = RoutingInput( + destination: destination, + memoType: input['memoType'].toString(), + memoValue: input['memoValue']?.toString(), + sourceAccount: input['sourceAccount']?.toString(), + ); + + try { + final result = extractRouting(routingInput); + + expect(result.destinationBaseAccount, + normalizeExpectedBaseAccount(expected['destinationBaseAccount'])); + + if (expected['routingId'] != null) { + expect(result.id, BigInt.parse(expected['routingId'].toString())); + } else { + expect(result.id, isNull); + } + + expect(result.source.name, expected['routingSource']); + + if (expected.containsKey('warnings')) { + final List expectedWarnings = + expected['warnings'] as List; + expect(result.warnings.length, expectedWarnings.length); + for (var i = 0; i < expectedWarnings.length; i++) { + final eW = expectedWarnings[i] as Map; + expect(result.warnings[i].code, eW['code']); + } + } + + if (expected.containsKey('destinationError')) { + final eE = expected['destinationError'] as Map; + expect(result.destinationError?.code, eE['code']); + } + } on ExtractRoutingException { + if (destination.startsWith('C')) { + // Expected for C-address vectors in this spec runner + } else { + rethrow; + } + } break; } }); } }); } +