diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/node_modules/.vite/vitest/results.json b/node_modules/.pnpm/node_modules/stellar-address-kit/node_modules/.vite/vitest/results.json index 2017d57..aaf2a02 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/node_modules/.vite/vitest/results.json +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":src/muxed/encode.test.ts",{"duration":13,"failed":false}],[":src/address/parse.test.ts",{"duration":20,"failed":false}],[":src/spec/validate.test.ts",{"duration":11,"failed":false}],[":src/spec/detect.test.ts",{"duration":14,"failed":false}],[":src/spec/runner.test.ts",{"duration":21,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":src/test/detect.test.ts",{"duration":24,"failed":false}],[":src/test/validate.test.ts",{"duration":26,"failed":false}],[":src/muxed/encode.test.ts",{"duration":18,"failed":false}],[":src/test/extract.test.ts",{"duration":51,"failed":false}],[":src/address/parse.test.ts",{"duration":45,"failed":false}],[":src/test/integration.test.ts",{"duration":71,"failed":false}],[":src/spec/runner.test.ts",{"duration":55,"failed":false}],[":src/spec/validate.test.ts",{"duration":8,"failed":false}],[":src/test/bigint-edge-cases.test.ts",{"duration":12,"failed":false}],[":src/spec/detect.test.ts",{"duration":13,"failed":false}]]} \ No newline at end of file diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/detect.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/detect.ts index a30c5a9..adc5166 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/detect.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/detect.ts @@ -5,14 +5,8 @@ const { StrKey } = StellarSdk; const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; /** - * @param input - The Base32-encoded string to decode. - * @returns A Uint8Array containing the decoded binary data. - * - * @throws {Error} If the input contains characters not found in the Base32 alphabet. - * - * @example - * const bytes = decodeBase32("MZXW6==="); // "foo" - * console.log(new TextDecoder().decode(bytes)); // "foo" + * Decodes a Base32-encoded string into binary data. + * @internal */ function decodeBase32(input: string): Uint8Array { const s = input.toUpperCase().replace(/=+$/, ""); @@ -38,23 +32,8 @@ function decodeBase32(input: string): Uint8Array { } /** - * Computes a 16-bit CRC (Cyclic Redundancy Check) for the given byte array. - * - * This implementation uses the CRC-16-CCITT polynomial (0x1021) with: - * - Initial value: 0x0000 - * - No reflection (input or output) - * - No final XOR - * - * The function processes each byte bit-by-bit, updating the CRC value - * using a shift register and polynomial XOR operations. - * - * @param bytes - The input data as a Uint8Array. - * @returns The computed 16-bit CRC value as a number (0–65535). - * - * @example - * const data = new Uint8Array([0x01, 0x02, 0x03]); - * const checksum = crc16(data); - * console.log(checksum); // e.g., 0x6131 + * Computes a 16-bit CRC (CCITT-FALSE) for binary validation. + * @internal */ function crc16(bytes: Uint8Array): number { let crc = 0; @@ -73,45 +52,24 @@ function crc16(bytes: Uint8Array): number { } /** - * Detects the type of a Stellar address. - * - * The function classifies the input string into one of the following: - * - `"G"`: Ed25519 public key (standard account address) - * - `"M"`: Med25519 (muxed) account address - * - `"C"`: Contract address - * - `"invalid"`: Not a valid or recognized address - * - * Detection is performed in two stages: - * 1. Uses official `StrKey` validation methods for known address types. - * 2. Falls back to manual validation for muxed (`"M"`) addresses by: - * - Decoding the Base32 string - * - Verifying structure (length and version byte) - * - Validating the CRC16 checksum - * - * @param address - The address string to evaluate. - * @returns A string indicating the detected address type or `"invalid"` if none match. - * - * @example - * detect("GBRPYHIL2C..."); // "G" - * detect("MA3D5F..."); // "M" - * detect("CA7Q..."); // "C" - * detect("invalid"); // "invalid" + * Identifies the Stellar address kind from a string input. + * Supports G (Ed25519), M (Muxed/SEP-23), and C (Contract) addresses. + * Returns "invalid" if the input does not match a supported format. */ export function detect(address: string): "G" | "M" | "C" | "invalid" { if (!address) return "invalid"; const up = address.toUpperCase(); - // 1. Try standard SDK validation (prioritize these) if (StrKey.isValidEd25519PublicKey(up)) return "G"; if (StrKey.isValidMed25519PublicKey(up)) return "M"; if (StrKey.isValidContract(up)) return "C"; - // 2. Fallback for custom 0x60 muxed addresses try { const prefix = up[0]; if (prefix === "M") { const decoded = decodeBase32(up); - // M-addresses are 43 bytes: 1 (version) + 32 (pubkey) + 8 (id) + 2 (checksum) + // M-addresses (SEP-23) payload consists of 1 version byte (0x60), + // 32-byte pubkey, 8-byte ID, and 2-byte CRC16 checksum. if (decoded.length === 43 && decoded[0] === 0x60) { const data = decoded.slice(0, decoded.length - 2); const checksum = @@ -122,7 +80,7 @@ export function detect(address: string): "G" | "M" | "C" | "invalid" { } } } catch { - // Ignore and proceed to return "invalid" + // Return "invalid" on any decoding failure. } return "invalid"; diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/errors.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/errors.ts index 49ab564..588cbfb 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/errors.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/errors.ts @@ -8,6 +8,10 @@ export type ErrorCode = | "FEDERATION_ADDRESS_NOT_SUPPORTED" | "UNKNOWN_PREFIX"; +/** + * Represents an error encountered during the parsing of a Stellar address. + * Includes a machine-readable ErrorCode and the original input string. + */ export class AddressParseError extends Error { code: ErrorCode; readonly input: string; diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/parse.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/parse.ts index 27bb8d5..9158985 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/parse.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/address/parse.ts @@ -5,49 +5,11 @@ import { AddressParseError } from "../address/errors"; /** * Parses a Stellar address string and returns a structured result. - * - * Parsing boundaries: - * - **G addresses** (pubkey): 56 characters, base32-encoded ed25519 public key with CRC16 checksum - * - **C addresses** (contract): 64 characters, base64-encoded contract ID - * - **M addresses** (muxed): Starts with "M", contains embedded ed25519 pubkey + muxed account ID - * - * The input is automatically normalized to uppercase before parsing. - * Invalid addresses result in thrown {@link AddressParseError} rather than - * returning an error result, making this the primary validation entrypoint. - * - * @param address - The Stellar address string to parse (G... , C... , or M... prefix) - * @returns A {@link ParseResult} containing the parsed address with kind and warnings - * @throws {AddressParseError} When the address fails validation: - * - `"UNKNOWN_PREFIX"` - Address does not start with G, M, or C - * - `"INVALID_CHECKSUM"` - Base32 decoding or CRC16 checksum validation failed (pubkey/muxed) - * - `"INVALID_LENGTH"` - Address length does not match expected format - * - `"INVALID_BASE32"` - Address contains non-base32 characters (pubkey/muxed) - * - * @example - * ```ts - * // Parse a public key - * const result = parse("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ"); - * // => { kind: "G", address: "GA7QYNF7...", warnings: [] } - * - * // Parse a contract address - * const contract = parse("CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"); - * // => { kind: "C", address: "CDLZFC3...", warnings: [] } - * - * // Parse a muxed address - * const muxed = parse("MBMM5N2TPABDA6OZHG5WSF6CXDABPK62L3DCMYC7QEYIIBPL6Q5DGDQ3"); - * // => { kind: "M", address: "...", baseG: "GA7QYNF...", muxedId: 12345n, warnings: [] } - * - * // Handle invalid addresses - * try { - * parse("INVALID"); - * } catch (error) { - * if (error instanceof AddressParseError) { - * console.log(error.code); // "UNKNOWN_PREFIX" - * console.log(error.input); // "INVALID" - * console.log(error.message); // "Invalid address" - * } - * } - * ``` + * Supports G (Public Key), C (Contract), and M (Muxed) addresses. + * + * @param address - The Stellar address string to parse. + * @returns A {@link ParseResult} containing the parsed address components. + * @throws {AddressParseError} If the address prefix is unknown or checksum/length validation fails. */ export function parse(address: string): ParseResult { const up = address.toUpperCase(); diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/decode.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/decode.ts index c8588b7..c13a389 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/decode.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/decode.ts @@ -2,6 +2,10 @@ import { StrKey } from "@stellar/stellar-sdk"; const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +/** + * Decodes a Base32-encoded string into binary data. + * @internal + */ function decodeBase32(input: string): Uint8Array { const s = input.toUpperCase().replace(/=+$/, ""); const byteCount = Math.floor((s.length * 5) / 8); @@ -24,6 +28,10 @@ function decodeBase32(input: string): Uint8Array { return result; } +/** + * Computes a 16-bit CRC (CCITT-FALSE). + * @internal + */ function crc16(bytes: Uint8Array): number { let crc = 0; for (const byte of bytes) { @@ -35,6 +43,10 @@ function crc16(bytes: Uint8Array): number { return crc & 0xffff; } +/** + * Validates and decodes a Stellar StrKey string. + * @internal + */ function decodeStrKey(address: string): Uint8Array { const up = address.toUpperCase(); const decoded = decodeBase32(up); @@ -50,15 +62,14 @@ function decodeStrKey(address: string): Uint8Array { } /** - * Decodes a muxed Stellar address into its base G address and numeric ID. - * Returns a typed structure to ensure native TypeScript protection and clean destructuring. + * Decodes a muxed Stellar address (SEP-23) into its base account and ID. + * The payload is expected to be [Version(1)] [Pubkey(32)] [ID(8)]. * - * @param mAddress The muxed Stellar address (starts with M). - * @returns An object containing the base G address and the 64-bit ID as a bigint. + * @param mAddress - The muxed address string starting with 'M'. + * @returns Metadata containing the base G address and the 64-bit BigInt ID. */ export function decodeMuxed(mAddress: string): { baseG: string; id: bigint } { const data = decodeStrKey(mAddress); - // Layout for Muxed Address: [Version(1)] [Pubkey(32)] [ID(8)] if (data.length !== 41) throw new Error("invalid payload length"); const pubkey = data.slice(1, 33); diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/encode.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/encode.ts index f1ee8a6..bc76330 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/encode.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/muxed/encode.ts @@ -1,34 +1,34 @@ import { StrKey } from "@stellar/stellar-sdk"; -// Use BigInt literal for the 64-bit unsigned integer maximum +/** + * Maximum value for a 64-bit unsigned integer. + */ const MAX_UINT64 = 18446744073709551615n; /** - * Encodes a muxed Stellar address using a base G address and numeric ID. - * Adheres to BigInt audit requirements to prevent precision loss. + * Encodes a base G address and numeric ID into a muxed Stellar address (SEP-23). + * Uses BigInt for the ID to prevent precision loss and ensures the ID is within uint64 boundaries. + * + * @param baseG - Ed25519 public key string starting with 'G'. + * @param id - The 64-bit routing ID as a BigInt. + * @returns The encoded muxed address string starting with 'M'. + * @throws {TypeError} If ID is not a BigInt. + * @throws {RangeError} If ID is outside the uint64 range [0, 2^64-1]. */ export function encodeMuxed(baseG: string, id: bigint): string { - // 1. Strict Type Enforcement - // Ensure we are working with a BigInt immediately if (typeof id !== "bigint") { throw new TypeError(`ID must be a bigint, received ${typeof id}`); } - // 2. Uint64 Boundary Check - // Using BigInt literals (0n) for comparison if (id < 0n || id > MAX_UINT64) { throw new RangeError(`ID is outside the uint64 range: 0 to ${MAX_UINT64}`); } - // 3. Address Validation if (!StrKey.isValidEd25519PublicKey(baseG)) { throw new Error(`Invalid base G address (Ed25519 public key expected)`); } - // 4. Safe Encoding - // Build the 40-byte med25519 payload directly: - // [ed25519 pubkey (32 bytes)] [uint64 id (8 bytes, big-endian)]. - // This avoids runtime constructor-contract drift in MuxedAccount across SDK versions. + // Build the 40-byte med25519 payload: [pubkey (32 bytes)] [uint64 id (8 bytes, BE)]. const pubkeyBytes = Buffer.from(StrKey.decodeEd25519PublicKey(baseG)); const idBytes = Buffer.alloc(8); idBytes.writeBigUInt64BE(id); diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/extract.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/extract.ts index 824ecde..eb3d3c2 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/extract.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/extract.ts @@ -33,15 +33,14 @@ function assertRoutableAddress(destination: string): void { } /** - * Extracts routing information from a given destination address and memo. + * Extracts deposit routing information from a Stellar address and memo. * - * @param input - The routing input containing the destination address, memo type, and memo value. - * @returns The extracted routing result including destination base account, routing ID, routing source, and warnings. + * Routing Policy: + * 1. M-addresses: Routing ID is extracted from the address; any memo is ignored for routing. + * 2. G-addresses: Routing ID is extracted from MEMO_ID or numeric MEMO_TEXT if valid. * - * Decision Branches: - * - M-address: The destination is parsed as a muxed account. The routing ID from the M-address takes precedence over any provided memo. - * - G-address with MEMO_ID: The destination is a standard G-address. The routing ID is extracted from the MEMO_ID. - * - G-address with MEMO_TEXT: The destination is a standard G-address. The routing ID is extracted from the MEMO_TEXT if it represents a valid numeric uint64. + * @param input - The destination address and optional memo components. + * @returns A result containing the base account, routing ID, source, and any warnings. */ export function extractRouting(input: RoutingInput): RoutingResult { assertRoutableAddress(input.destination); diff --git a/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/memo.ts b/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/memo.ts index 0acb6d7..e0c561d 100644 --- a/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/memo.ts +++ b/node_modules/.pnpm/node_modules/stellar-address-kit/src/routing/memo.ts @@ -5,8 +5,18 @@ export type NormalizeResult = { warnings: Warning[]; }; +/** + * Maximum value for a 64-bit unsigned integer (uint64). + */ const UINT64_MAX = BigInt("18446744073709551615"); +/** + * Normalizes a numeric string into a canonical uint64 representation. + * Strips leading zeros and validates that the value is within uint64 boundaries. + * + * @param s - The numeric string to normalize. + * @returns Result containing the normalized string (or null if invalid) and any warnings. + */ export function normalizeMemoTextId(s: string): NormalizeResult { const warnings: Warning[] = []; diff --git a/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= b/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= index 2f881d8..b6117f8 100644 Binary files a/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= and b/packages/core-dart/.dart_tool/test/incremental_kernel.Ly9AZGFydD0zLjA= differ diff --git a/packages/core-dart/lib/src/address/parse.dart b/packages/core-dart/lib/src/address/parse.dart index 31802d9..74bfa00 100644 --- a/packages/core-dart/lib/src/address/parse.dart +++ b/packages/core-dart/lib/src/address/parse.dart @@ -1,6 +1,9 @@ import 'detect.dart'; import 'codes.dart'; +/// Parses a [String] into a [ParseResult]. +/// Normalizes the input to uppercase and returns any applicable warnings +/// or errors if the format is unrecognized. ParseResult parse(String input) { final kind = detect(input); if (kind == null) { diff --git a/packages/core-dart/lib/src/routing/extract.dart b/packages/core-dart/lib/src/routing/extract.dart index 219c190..e80caf5 100644 --- a/packages/core-dart/lib/src/routing/extract.dart +++ b/packages/core-dart/lib/src/routing/extract.dart @@ -4,6 +4,9 @@ import '../muxed/decode.dart'; import '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) { if (input.sourceAccount != null && input.sourceAccount!.isNotEmpty) { final source = parse(input.sourceAccount!); diff --git a/packages/core-dart/test/debug_m.dart b/packages/core-dart/test/debug_m.dart index 5b31c5a..6ec58cd 100644 --- a/packages/core-dart/test/debug_m.dart +++ b/packages/core-dart/test/debug_m.dart @@ -1,6 +1,4 @@ -import 'package:stellar_address_kit/stellar_address_kit.dart'; -import '../lib/src/util/strkey.dart'; -import 'dart:typed_data'; +import 'package:stellar_address_kit/src/util/strkey.dart'; void main() { const m = diff --git a/packages/core-dart/test/muxed_id_range_test.dart b/packages/core-dart/test/muxed_id_range_test.dart index def8cbf..464feea 100644 --- a/packages/core-dart/test/muxed_id_range_test.dart +++ b/packages/core-dart/test/muxed_id_range_test.dart @@ -17,6 +17,7 @@ /// /// These tests MUST PASS on both unfixed and fixed code — they capture the /// baseline behavior that the fix must not regress. +library; import 'package:test/test.dart'; import 'package:stellar_address_kit/stellar_address_kit.dart'; @@ -27,18 +28,21 @@ void main() { final uint64Max = BigInt.parse('18446744073709551615'); - group('MuxedAddress.encode — out-of-range id (bug condition exploration)', () { + group('MuxedAddress.encode — out-of-range id (bug condition exploration)', + () { /// Validates: Requirements 1.1, 1.2 test('throws StellarAddressException for id = -1 (negative)', () { expect( () => MuxedAddress.encode(baseG: validG, id: BigInt.from(-1)), throwsA(isA()), - reason: 'encode(validG, -1) should throw but returns a String on unfixed code', + reason: + 'encode(validG, -1) should throw but returns a String on unfixed code', ); }); /// Validates: Requirements 1.2 - test('throws StellarAddressException for id = 2^64 (one above uint64Max)', () { + test('throws StellarAddressException for id = 2^64 (one above uint64Max)', + () { expect( () => MuxedAddress.encode( baseG: validG, @@ -51,7 +55,9 @@ void main() { }); /// Validates: Requirements 1.1 - test('throws StellarAddressException for id = -9999999999999999999 (large negative)', () { + test( + 'throws StellarAddressException for id = -9999999999999999999 (large negative)', + () { expect( () => MuxedAddress.encode( baseG: validG, @@ -90,14 +96,14 @@ void main() { // Representative valid ids covering lower boundary, upper boundary, and // several mid-range values. final validIds = [ - BigInt.zero, // lower boundary (req 3.1) - BigInt.one, // just above zero - BigInt.from(1000), // small mid-range - BigInt.parse('9223372036854775807'), // int64 max (mid-range) - BigInt.parse('9223372036854775808'), // int64 max + 1 - BigInt.parse('12345678901234567890'), // large mid-range - uint64Max - BigInt.one, // one below upper boundary - uint64Max, // upper boundary (req 3.3) + BigInt.zero, // lower boundary (req 3.1) + BigInt.one, // just above zero + BigInt.from(1000), // small mid-range + BigInt.parse('9223372036854775807'), // int64 max (mid-range) + BigInt.parse('9223372036854775808'), // int64 max + 1 + BigInt.parse('12345678901234567890'), // large mid-range + uint64Max - BigInt.one, // one below upper boundary + uint64Max, // upper boundary (req 3.3) ]; /// Validates: Requirements 3.1, 3.2, 3.3 @@ -115,24 +121,29 @@ void main() { /// Validates: Requirement 3.4 /// encode with an invalid baseG must throw StellarAddressException for the /// base address, regardless of the id value. - test('encode(invalidG, validId) throws StellarAddressException for invalid base', () { + test( + 'encode(invalidG, validId) throws StellarAddressException for invalid base', + () { const invalidG = 'INVALID_ADDRESS'; expect( () => MuxedAddress.encode(baseG: invalidG, id: BigInt.from(42)), throwsA(isA()), - reason: 'encode with an invalid G-address must throw StellarAddressException', + reason: + 'encode with an invalid G-address must throw StellarAddressException', ); }); /// Validates: Requirement 3.4 — also check with a well-formed but wrong-type address - test('encode(M-address as baseG, validId) throws StellarAddressException', () { + test('encode(M-address as baseG, validId) throws StellarAddressException', + () { // An M-address is not a valid baseG const mAddress = 'MAYCUYT553C5LHVE2XPW5GMEJT4BXGM7AHMJWLAPZP53KJO7EIQACAAAAAAAAAAAAD672'; expect( () => MuxedAddress.encode(baseG: mAddress, id: BigInt.zero), throwsA(isA()), - reason: 'encode with an M-address as baseG must throw StellarAddressException', + reason: + 'encode with an M-address as baseG must throw StellarAddressException', ); }); }); diff --git a/packages/core-go/address/detect.go b/packages/core-go/address/detect.go index b8aadab..338db7f 100644 --- a/packages/core-go/address/detect.go +++ b/packages/core-go/address/detect.go @@ -1,6 +1,7 @@ package address // Detect identifies the AddressKind of a Stellar address string. +// Supported kinds include KindG (Ed25519), KindM (Muxed/SEP-23), and KindC (Contract). func Detect(addr string) (AddressKind, error) { versionByte, _, err := DecodeStrKey(addr) if err != nil { diff --git a/packages/core-go/address/parse.go b/packages/core-go/address/parse.go index 8a3420c..c552f4f 100644 --- a/packages/core-go/address/parse.go +++ b/packages/core-go/address/parse.go @@ -5,10 +5,9 @@ import ( "strings" ) -// Parse parses a Stellar address into an Address struct. -// -// For muxed accounts (M-addresses), BaseG and MuxedID are populated. -// For other kinds, BaseG will be empty and MuxedID will be zero. +// Parse parses a Stellar address string into an Address struct. +// For muxed accounts (KindM), BaseG and MuxedID are populated; for other kinds, +// these fields will be empty/zero. Returns an AddressError if validation fails. func Parse(input string) (*Address, error) { kind, err := Detect(input) if err != nil { diff --git a/packages/core-go/routing/extract.go b/packages/core-go/routing/extract.go index e6b1125..4f7b8b1 100644 --- a/packages/core-go/routing/extract.go +++ b/packages/core-go/routing/extract.go @@ -27,6 +27,10 @@ func normalizeUnsupportedMemoType(memoType string) string { } } +// ExtractRouting identifies the deposit routing destination and identifier from a Stellar +// payment input. It implements the standard priority policy where M-address identifiers +// take precedence over any provided memo. Returns a RoutingResult with the decoded +// state and applicable warnings. func ExtractRouting(input RoutingInput) RoutingResult { if input.SourceAccount != "" { source, err := address.Parse(input.SourceAccount) diff --git a/packages/core-ts/node_modules/.vite/vitest/results.json b/packages/core-ts/node_modules/.vite/vitest/results.json index 2017d57..aaf2a02 100644 --- a/packages/core-ts/node_modules/.vite/vitest/results.json +++ b/packages/core-ts/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":src/muxed/encode.test.ts",{"duration":13,"failed":false}],[":src/address/parse.test.ts",{"duration":20,"failed":false}],[":src/spec/validate.test.ts",{"duration":11,"failed":false}],[":src/spec/detect.test.ts",{"duration":14,"failed":false}],[":src/spec/runner.test.ts",{"duration":21,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":src/test/detect.test.ts",{"duration":24,"failed":false}],[":src/test/validate.test.ts",{"duration":26,"failed":false}],[":src/muxed/encode.test.ts",{"duration":18,"failed":false}],[":src/test/extract.test.ts",{"duration":51,"failed":false}],[":src/address/parse.test.ts",{"duration":45,"failed":false}],[":src/test/integration.test.ts",{"duration":71,"failed":false}],[":src/spec/runner.test.ts",{"duration":55,"failed":false}],[":src/spec/validate.test.ts",{"duration":8,"failed":false}],[":src/test/bigint-edge-cases.test.ts",{"duration":12,"failed":false}],[":src/spec/detect.test.ts",{"duration":13,"failed":false}]]} \ No newline at end of file diff --git a/packages/core-ts/src/address/detect.ts b/packages/core-ts/src/address/detect.ts index a30c5a9..adc5166 100644 --- a/packages/core-ts/src/address/detect.ts +++ b/packages/core-ts/src/address/detect.ts @@ -5,14 +5,8 @@ const { StrKey } = StellarSdk; const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; /** - * @param input - The Base32-encoded string to decode. - * @returns A Uint8Array containing the decoded binary data. - * - * @throws {Error} If the input contains characters not found in the Base32 alphabet. - * - * @example - * const bytes = decodeBase32("MZXW6==="); // "foo" - * console.log(new TextDecoder().decode(bytes)); // "foo" + * Decodes a Base32-encoded string into binary data. + * @internal */ function decodeBase32(input: string): Uint8Array { const s = input.toUpperCase().replace(/=+$/, ""); @@ -38,23 +32,8 @@ function decodeBase32(input: string): Uint8Array { } /** - * Computes a 16-bit CRC (Cyclic Redundancy Check) for the given byte array. - * - * This implementation uses the CRC-16-CCITT polynomial (0x1021) with: - * - Initial value: 0x0000 - * - No reflection (input or output) - * - No final XOR - * - * The function processes each byte bit-by-bit, updating the CRC value - * using a shift register and polynomial XOR operations. - * - * @param bytes - The input data as a Uint8Array. - * @returns The computed 16-bit CRC value as a number (0–65535). - * - * @example - * const data = new Uint8Array([0x01, 0x02, 0x03]); - * const checksum = crc16(data); - * console.log(checksum); // e.g., 0x6131 + * Computes a 16-bit CRC (CCITT-FALSE) for binary validation. + * @internal */ function crc16(bytes: Uint8Array): number { let crc = 0; @@ -73,45 +52,24 @@ function crc16(bytes: Uint8Array): number { } /** - * Detects the type of a Stellar address. - * - * The function classifies the input string into one of the following: - * - `"G"`: Ed25519 public key (standard account address) - * - `"M"`: Med25519 (muxed) account address - * - `"C"`: Contract address - * - `"invalid"`: Not a valid or recognized address - * - * Detection is performed in two stages: - * 1. Uses official `StrKey` validation methods for known address types. - * 2. Falls back to manual validation for muxed (`"M"`) addresses by: - * - Decoding the Base32 string - * - Verifying structure (length and version byte) - * - Validating the CRC16 checksum - * - * @param address - The address string to evaluate. - * @returns A string indicating the detected address type or `"invalid"` if none match. - * - * @example - * detect("GBRPYHIL2C..."); // "G" - * detect("MA3D5F..."); // "M" - * detect("CA7Q..."); // "C" - * detect("invalid"); // "invalid" + * Identifies the Stellar address kind from a string input. + * Supports G (Ed25519), M (Muxed/SEP-23), and C (Contract) addresses. + * Returns "invalid" if the input does not match a supported format. */ export function detect(address: string): "G" | "M" | "C" | "invalid" { if (!address) return "invalid"; const up = address.toUpperCase(); - // 1. Try standard SDK validation (prioritize these) if (StrKey.isValidEd25519PublicKey(up)) return "G"; if (StrKey.isValidMed25519PublicKey(up)) return "M"; if (StrKey.isValidContract(up)) return "C"; - // 2. Fallback for custom 0x60 muxed addresses try { const prefix = up[0]; if (prefix === "M") { const decoded = decodeBase32(up); - // M-addresses are 43 bytes: 1 (version) + 32 (pubkey) + 8 (id) + 2 (checksum) + // M-addresses (SEP-23) payload consists of 1 version byte (0x60), + // 32-byte pubkey, 8-byte ID, and 2-byte CRC16 checksum. if (decoded.length === 43 && decoded[0] === 0x60) { const data = decoded.slice(0, decoded.length - 2); const checksum = @@ -122,7 +80,7 @@ export function detect(address: string): "G" | "M" | "C" | "invalid" { } } } catch { - // Ignore and proceed to return "invalid" + // Return "invalid" on any decoding failure. } return "invalid"; diff --git a/packages/core-ts/src/address/errors.ts b/packages/core-ts/src/address/errors.ts index 49ab564..588cbfb 100644 --- a/packages/core-ts/src/address/errors.ts +++ b/packages/core-ts/src/address/errors.ts @@ -8,6 +8,10 @@ export type ErrorCode = | "FEDERATION_ADDRESS_NOT_SUPPORTED" | "UNKNOWN_PREFIX"; +/** + * Represents an error encountered during the parsing of a Stellar address. + * Includes a machine-readable ErrorCode and the original input string. + */ export class AddressParseError extends Error { code: ErrorCode; readonly input: string; diff --git a/packages/core-ts/src/address/parse.ts b/packages/core-ts/src/address/parse.ts index 27bb8d5..9158985 100644 --- a/packages/core-ts/src/address/parse.ts +++ b/packages/core-ts/src/address/parse.ts @@ -5,49 +5,11 @@ import { AddressParseError } from "../address/errors"; /** * Parses a Stellar address string and returns a structured result. - * - * Parsing boundaries: - * - **G addresses** (pubkey): 56 characters, base32-encoded ed25519 public key with CRC16 checksum - * - **C addresses** (contract): 64 characters, base64-encoded contract ID - * - **M addresses** (muxed): Starts with "M", contains embedded ed25519 pubkey + muxed account ID - * - * The input is automatically normalized to uppercase before parsing. - * Invalid addresses result in thrown {@link AddressParseError} rather than - * returning an error result, making this the primary validation entrypoint. - * - * @param address - The Stellar address string to parse (G... , C... , or M... prefix) - * @returns A {@link ParseResult} containing the parsed address with kind and warnings - * @throws {AddressParseError} When the address fails validation: - * - `"UNKNOWN_PREFIX"` - Address does not start with G, M, or C - * - `"INVALID_CHECKSUM"` - Base32 decoding or CRC16 checksum validation failed (pubkey/muxed) - * - `"INVALID_LENGTH"` - Address length does not match expected format - * - `"INVALID_BASE32"` - Address contains non-base32 characters (pubkey/muxed) - * - * @example - * ```ts - * // Parse a public key - * const result = parse("GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ"); - * // => { kind: "G", address: "GA7QYNF7...", warnings: [] } - * - * // Parse a contract address - * const contract = parse("CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"); - * // => { kind: "C", address: "CDLZFC3...", warnings: [] } - * - * // Parse a muxed address - * const muxed = parse("MBMM5N2TPABDA6OZHG5WSF6CXDABPK62L3DCMYC7QEYIIBPL6Q5DGDQ3"); - * // => { kind: "M", address: "...", baseG: "GA7QYNF...", muxedId: 12345n, warnings: [] } - * - * // Handle invalid addresses - * try { - * parse("INVALID"); - * } catch (error) { - * if (error instanceof AddressParseError) { - * console.log(error.code); // "UNKNOWN_PREFIX" - * console.log(error.input); // "INVALID" - * console.log(error.message); // "Invalid address" - * } - * } - * ``` + * Supports G (Public Key), C (Contract), and M (Muxed) addresses. + * + * @param address - The Stellar address string to parse. + * @returns A {@link ParseResult} containing the parsed address components. + * @throws {AddressParseError} If the address prefix is unknown or checksum/length validation fails. */ export function parse(address: string): ParseResult { const up = address.toUpperCase(); diff --git a/packages/core-ts/src/muxed/decode.ts b/packages/core-ts/src/muxed/decode.ts index c8588b7..c13a389 100644 --- a/packages/core-ts/src/muxed/decode.ts +++ b/packages/core-ts/src/muxed/decode.ts @@ -2,6 +2,10 @@ import { StrKey } from "@stellar/stellar-sdk"; const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +/** + * Decodes a Base32-encoded string into binary data. + * @internal + */ function decodeBase32(input: string): Uint8Array { const s = input.toUpperCase().replace(/=+$/, ""); const byteCount = Math.floor((s.length * 5) / 8); @@ -24,6 +28,10 @@ function decodeBase32(input: string): Uint8Array { return result; } +/** + * Computes a 16-bit CRC (CCITT-FALSE). + * @internal + */ function crc16(bytes: Uint8Array): number { let crc = 0; for (const byte of bytes) { @@ -35,6 +43,10 @@ function crc16(bytes: Uint8Array): number { return crc & 0xffff; } +/** + * Validates and decodes a Stellar StrKey string. + * @internal + */ function decodeStrKey(address: string): Uint8Array { const up = address.toUpperCase(); const decoded = decodeBase32(up); @@ -50,15 +62,14 @@ function decodeStrKey(address: string): Uint8Array { } /** - * Decodes a muxed Stellar address into its base G address and numeric ID. - * Returns a typed structure to ensure native TypeScript protection and clean destructuring. + * Decodes a muxed Stellar address (SEP-23) into its base account and ID. + * The payload is expected to be [Version(1)] [Pubkey(32)] [ID(8)]. * - * @param mAddress The muxed Stellar address (starts with M). - * @returns An object containing the base G address and the 64-bit ID as a bigint. + * @param mAddress - The muxed address string starting with 'M'. + * @returns Metadata containing the base G address and the 64-bit BigInt ID. */ export function decodeMuxed(mAddress: string): { baseG: string; id: bigint } { const data = decodeStrKey(mAddress); - // Layout for Muxed Address: [Version(1)] [Pubkey(32)] [ID(8)] if (data.length !== 41) throw new Error("invalid payload length"); const pubkey = data.slice(1, 33); diff --git a/packages/core-ts/src/muxed/encode.ts b/packages/core-ts/src/muxed/encode.ts index f1ee8a6..bc76330 100644 --- a/packages/core-ts/src/muxed/encode.ts +++ b/packages/core-ts/src/muxed/encode.ts @@ -1,34 +1,34 @@ import { StrKey } from "@stellar/stellar-sdk"; -// Use BigInt literal for the 64-bit unsigned integer maximum +/** + * Maximum value for a 64-bit unsigned integer. + */ const MAX_UINT64 = 18446744073709551615n; /** - * Encodes a muxed Stellar address using a base G address and numeric ID. - * Adheres to BigInt audit requirements to prevent precision loss. + * Encodes a base G address and numeric ID into a muxed Stellar address (SEP-23). + * Uses BigInt for the ID to prevent precision loss and ensures the ID is within uint64 boundaries. + * + * @param baseG - Ed25519 public key string starting with 'G'. + * @param id - The 64-bit routing ID as a BigInt. + * @returns The encoded muxed address string starting with 'M'. + * @throws {TypeError} If ID is not a BigInt. + * @throws {RangeError} If ID is outside the uint64 range [0, 2^64-1]. */ export function encodeMuxed(baseG: string, id: bigint): string { - // 1. Strict Type Enforcement - // Ensure we are working with a BigInt immediately if (typeof id !== "bigint") { throw new TypeError(`ID must be a bigint, received ${typeof id}`); } - // 2. Uint64 Boundary Check - // Using BigInt literals (0n) for comparison if (id < 0n || id > MAX_UINT64) { throw new RangeError(`ID is outside the uint64 range: 0 to ${MAX_UINT64}`); } - // 3. Address Validation if (!StrKey.isValidEd25519PublicKey(baseG)) { throw new Error(`Invalid base G address (Ed25519 public key expected)`); } - // 4. Safe Encoding - // Build the 40-byte med25519 payload directly: - // [ed25519 pubkey (32 bytes)] [uint64 id (8 bytes, big-endian)]. - // This avoids runtime constructor-contract drift in MuxedAccount across SDK versions. + // Build the 40-byte med25519 payload: [pubkey (32 bytes)] [uint64 id (8 bytes, BE)]. const pubkeyBytes = Buffer.from(StrKey.decodeEd25519PublicKey(baseG)); const idBytes = Buffer.alloc(8); idBytes.writeBigUInt64BE(id); diff --git a/packages/core-ts/src/routing/extract.ts b/packages/core-ts/src/routing/extract.ts index 824ecde..eb3d3c2 100644 --- a/packages/core-ts/src/routing/extract.ts +++ b/packages/core-ts/src/routing/extract.ts @@ -33,15 +33,14 @@ function assertRoutableAddress(destination: string): void { } /** - * Extracts routing information from a given destination address and memo. + * Extracts deposit routing information from a Stellar address and memo. * - * @param input - The routing input containing the destination address, memo type, and memo value. - * @returns The extracted routing result including destination base account, routing ID, routing source, and warnings. + * Routing Policy: + * 1. M-addresses: Routing ID is extracted from the address; any memo is ignored for routing. + * 2. G-addresses: Routing ID is extracted from MEMO_ID or numeric MEMO_TEXT if valid. * - * Decision Branches: - * - M-address: The destination is parsed as a muxed account. The routing ID from the M-address takes precedence over any provided memo. - * - G-address with MEMO_ID: The destination is a standard G-address. The routing ID is extracted from the MEMO_ID. - * - G-address with MEMO_TEXT: The destination is a standard G-address. The routing ID is extracted from the MEMO_TEXT if it represents a valid numeric uint64. + * @param input - The destination address and optional memo components. + * @returns A result containing the base account, routing ID, source, and any warnings. */ export function extractRouting(input: RoutingInput): RoutingResult { assertRoutableAddress(input.destination); diff --git a/packages/core-ts/src/routing/memo.ts b/packages/core-ts/src/routing/memo.ts index 0acb6d7..e0c561d 100644 --- a/packages/core-ts/src/routing/memo.ts +++ b/packages/core-ts/src/routing/memo.ts @@ -5,8 +5,18 @@ export type NormalizeResult = { warnings: Warning[]; }; +/** + * Maximum value for a 64-bit unsigned integer (uint64). + */ const UINT64_MAX = BigInt("18446744073709551615"); +/** + * Normalizes a numeric string into a canonical uint64 representation. + * Strips leading zeros and validates that the value is within uint64 boundaries. + * + * @param s - The numeric string to normalize. + * @returns Result containing the normalized string (or null if invalid) and any warnings. + */ export function normalizeMemoTextId(s: string): NormalizeResult { const warnings: Warning[] = [];