@bcts/uniform-resources 1.0.0-alpha.9 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,10 @@
1
- import { decodeCbor } from "@bcts/dcbor";
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
7
+ import { decodeCbor, MajorType, type Cbor } from "@bcts/dcbor";
2
8
  import { InvalidSchemeError, InvalidTypeError, UnexpectedTypeError, URError } from "./error.js";
3
9
  import { UR } from "./ur.js";
4
10
  import { URType } from "./ur-type.js";
@@ -118,27 +124,43 @@ export class MultipartDecoder {
118
124
 
119
125
  /**
120
126
  * Decodes a multipart UR's fountain part data.
127
+ *
128
+ * The multipart body is a CBOR array: [seqNum, seqLen, messageLen, checksum, data]
121
129
  */
122
130
  private _decodeFountainPart(partInfo: MultipartInfo): FountainPart {
123
- // Decode bytewords
124
- const rawData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
131
+ // Decode bytewords to get CBOR data
132
+ const cborData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
125
133
 
126
- if (rawData.length < 8) {
127
- throw new URError("Invalid multipart data: too short");
128
- }
134
+ // Decode the CBOR array
135
+ const decoded = decodeCbor(cborData);
129
136
 
130
- // Extract metadata
131
- const messageLen =
132
- ((rawData[0] << 24) | (rawData[1] << 16) | (rawData[2] << 8) | rawData[3]) >>> 0;
137
+ // The decoded value should be an array with 5 elements
138
+ if (decoded.type !== MajorType.Array) {
139
+ throw new URError("Invalid multipart data: expected CBOR array");
140
+ }
133
141
 
134
- const checksum =
135
- ((rawData[4] << 24) | (rawData[5] << 16) | (rawData[6] << 8) | rawData[7]) >>> 0;
142
+ const items = decoded.value as Cbor[];
143
+ if (items.length !== 5) {
144
+ throw new URError(`Invalid multipart data: expected 5 elements, got ${items.length}`);
145
+ }
136
146
 
137
- const data = rawData.slice(8);
147
+ // Extract the fields: [seqNum, seqLen, messageLen, checksum, data]
148
+ const seqNum = Number(items[0].value);
149
+ const seqLen = Number(items[1].value);
150
+ const messageLen = Number(items[2].value);
151
+ const checksum = Number(items[3].value);
152
+ const data = items[4].value as Uint8Array;
153
+
154
+ // Verify seqNum and seqLen match the URL path values
155
+ if (seqNum !== partInfo.seqNum || seqLen !== partInfo.seqLen) {
156
+ throw new URError(
157
+ `Multipart metadata mismatch: URL says ${partInfo.seqNum}-${partInfo.seqLen}, CBOR says ${seqNum}-${seqLen}`,
158
+ );
159
+ }
138
160
 
139
161
  return {
140
- seqNum: partInfo.seqNum,
141
- seqLen: partInfo.seqLen,
162
+ seqNum,
163
+ seqLen,
142
164
  messageLen,
143
165
  checksum,
144
166
  data,
@@ -160,28 +182,6 @@ export class MultipartDecoder {
160
182
  message(): UR | null {
161
183
  return this._decodedMessage;
162
184
  }
163
-
164
- /**
165
- * Returns the decoding progress as a fraction (0 to 1).
166
- */
167
- progress(): number {
168
- if (this._decodedMessage !== null) {
169
- return 1;
170
- }
171
- if (this._fountainDecoder === null) {
172
- return 0;
173
- }
174
- return this._fountainDecoder.progress();
175
- }
176
-
177
- /**
178
- * Resets the decoder to receive a new message.
179
- */
180
- reset(): void {
181
- this._urType = null;
182
- this._fountainDecoder = null;
183
- this._decodedMessage = null;
184
- }
185
185
  }
186
186
 
187
187
  /**
@@ -1,7 +1,14 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import type { UR } from "./ur.js";
2
8
  import { URError } from "./error.js";
3
9
  import { FountainEncoder, type FountainPart } from "./fountain.js";
4
10
  import { encodeBytewords, BytewordsStyle } from "./utils.js";
11
+ import { cbor } from "@bcts/dcbor";
5
12
 
6
13
  /**
7
14
  * Encodes a UR as multiple parts using fountain codes.
@@ -58,15 +65,6 @@ export class MultipartEncoder {
58
65
  this._fountainEncoder = new FountainEncoder(cborData, maxFragmentLen);
59
66
  }
60
67
 
61
- /**
62
- * Returns whether the message fits in a single part.
63
- *
64
- * For single-part messages, consider using UR.string() directly.
65
- */
66
- isSinglePart(): boolean {
67
- return this._fountainEncoder.isSinglePart();
68
- }
69
-
70
68
  /**
71
69
  * Gets the next part of the encoding.
72
70
  *
@@ -89,44 +87,28 @@ export class MultipartEncoder {
89
87
 
90
88
  /**
91
89
  * Encodes a fountain part as a UR string.
90
+ *
91
+ * Always emits the multipart `ur:<type>/<seqNum>-<seqLen>/<bytewords>`
92
+ * format — including for single-part messages (`1-1/...`). This mirrors
93
+ * Rust's `bc_ur::MultipartEncoder::next_part`, which never short-circuits
94
+ * to plain UR. Callers that want plain UR for tiny payloads should use
95
+ * `UR.string()` directly instead of constructing a `MultipartEncoder`.
92
96
  */
93
97
  private _encodePart(part: FountainPart): string {
94
- // For single-part messages, use simple format
95
- if (part.seqLen === 1) {
96
- return this._ur.string();
97
- }
98
-
99
- // Encode the part data as CBOR: [seqNum, seqLen, messageLen, checksum, data]
100
- // Using a simple format: seqNum-seqLen prefix followed by bytewords-encoded part
101
98
  const partData = this._encodePartData(part);
102
99
  const encoded = encodeBytewords(partData, BytewordsStyle.Minimal);
103
-
104
100
  return `ur:${this._ur.urTypeStr()}/${part.seqNum}-${part.seqLen}/${encoded}`;
105
101
  }
106
102
 
107
103
  /**
108
- * Encodes part metadata and data into bytes for bytewords encoding.
104
+ * Encodes part metadata and data as CBOR for bytewords encoding.
105
+ * Format: CBOR array [seqNum, seqLen, messageLen, checksum, data]
109
106
  */
110
107
  private _encodePartData(part: FountainPart): Uint8Array {
111
- // Simple encoding: messageLen (4 bytes) + checksum (4 bytes) + data
112
- const result = new Uint8Array(8 + part.data.length);
108
+ // Create CBOR array with 5 elements: [seqNum, seqLen, messageLen, checksum, data]
109
+ const cborArray = cbor([part.seqNum, part.seqLen, part.messageLen, part.checksum, part.data]);
113
110
 
114
- // Message length (big-endian)
115
- result[0] = (part.messageLen >>> 24) & 0xff;
116
- result[1] = (part.messageLen >>> 16) & 0xff;
117
- result[2] = (part.messageLen >>> 8) & 0xff;
118
- result[3] = part.messageLen & 0xff;
119
-
120
- // Checksum (big-endian)
121
- result[4] = (part.checksum >>> 24) & 0xff;
122
- result[5] = (part.checksum >>> 16) & 0xff;
123
- result[6] = (part.checksum >>> 8) & 0xff;
124
- result[7] = part.checksum & 0xff;
125
-
126
- // Fragment data
127
- result.set(part.data, 8);
128
-
129
- return result;
111
+ return cborArray.toData();
130
112
  }
131
113
 
132
114
  /**
@@ -145,22 +127,4 @@ export class MultipartEncoder {
145
127
  partsCount(): number {
146
128
  return this._fountainEncoder.seqLen;
147
129
  }
148
-
149
- /**
150
- * Checks if all pure parts have been emitted.
151
- *
152
- * Even after this returns true, you can continue calling nextPart()
153
- * to generate additional rateless parts for redundancy.
154
- */
155
- isComplete(): boolean {
156
- return this._fountainEncoder.isComplete();
157
- }
158
-
159
- /**
160
- * Resets the encoder to start from the beginning.
161
- */
162
- reset(): void {
163
- this._currentIndex = 0;
164
- this._fountainEncoder.reset();
165
- }
166
130
  }
package/src/ur-codable.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import type { UREncodable } from "./ur-encodable.js";
2
8
  import type { URDecodable } from "./ur-decodable.js";
3
9
 
@@ -1,4 +1,12 @@
1
- import type { UR } from "./ur.js";
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
7
+ import type { CborTaggedDecodable } from "@bcts/dcbor";
8
+
9
+ import { UR } from "./ur.js";
2
10
 
3
11
  /**
4
12
  * A type that can be decoded from a UR (Uniform Resource).
@@ -6,17 +14,26 @@ import type { UR } from "./ur.js";
6
14
  * Types implementing this interface should be able to create themselves
7
15
  * from a UR containing their data.
8
16
  *
17
+ * Mirrors Rust's `URDecodable` trait (`bc-ur-rust/src/ur_decodable.rs`),
18
+ * which has a blanket impl `impl<T> URDecodable for T where T:
19
+ * CBORTaggedDecodable`. TypeScript has no equivalent of blanket impls, so
20
+ * implementers either write `fromUR()` directly *or* — for a type that
21
+ * already implements `CborTaggedDecodable` — call the helper functions
22
+ * {@link decodableFromUR} / {@link decodableFromURString} below to get the
23
+ * same auto-derivation that Rust provides for free.
24
+ *
9
25
  * @example
10
26
  * ```typescript
11
- * class MyType implements URDecodable {
12
- * fromUR(ur: UR): MyType {
13
- * const cbor = ur.cbor();
14
- * // Decode from CBOR and return MyType instance
27
+ * class MyType implements URDecodable, CborTaggedDecodable<MyType> {
28
+ * cborTags(): Tag[] {
29
+ * return [createTag(40000, "mytype")];
15
30
  * }
31
+ * fromUntaggedCbor(cbor: Cbor): MyType { ... }
32
+ * fromTaggedCbor(cbor: Cbor): MyType { ... }
16
33
  *
17
- * fromURString(urString: string): MyType {
18
- * return this.fromUR(UR.fromURString(urString));
19
- * }
34
+ * // Auto-derived from the first cbor tag's name, matching Rust.
35
+ * fromUR(ur: UR): MyType { return decodableFromUR(this, ur); }
36
+ * fromURString(s: string): MyType { return decodableFromURString(this, s); }
20
37
  * }
21
38
  * ```
22
39
  */
@@ -43,6 +60,41 @@ export interface URDecodable {
43
60
  fromURString?(urString: string): unknown;
44
61
  }
45
62
 
63
+ /**
64
+ * Concrete equivalent of Rust's default `URDecodable::from_ur` impl
65
+ * (`bc-ur-rust/src/ur_decodable.rs:7-15`):
66
+ *
67
+ * 1. Read the first tag returned by `decodable.cborTags()`.
68
+ * 2. Verify the UR's type matches that tag's name via `UR#checkType`
69
+ * (this is what Rust's `ur.check_type(...)` does — surface
70
+ * `UnexpectedTypeError` on mismatch).
71
+ * 3. Delegate to `decodable.fromUntaggedCbor(ur.cbor())`.
72
+ *
73
+ * Use from a class implementing both `URDecodable` and
74
+ * `CborTaggedDecodable<T>` to skip the type-check / delegate boilerplate.
75
+ */
76
+ export function decodableFromUR<T>(decodable: CborTaggedDecodable<T>, ur: UR): T {
77
+ const tags = decodable.cborTags();
78
+ const tag = tags[0];
79
+ if (tag === undefined) {
80
+ throw new Error("URDecodable: cborTags() returned no tags");
81
+ }
82
+ if (tag.name === undefined) {
83
+ throw new Error(`CBOR tag ${tag.value} must have a name. Did you call register_tags()?`);
84
+ }
85
+ ur.checkType(tag.name);
86
+ return decodable.fromUntaggedCbor(ur.cbor());
87
+ }
88
+
89
+ /**
90
+ * Concrete equivalent of Rust's default `URDecodable::from_ur_string` impl
91
+ * (`bc-ur-rust/src/ur_decodable.rs:17-22`):
92
+ * `Self::from_ur(UR::from_ur_string(s)?)`.
93
+ */
94
+ export function decodableFromURString<T>(decodable: CborTaggedDecodable<T>, urString: string): T {
95
+ return decodableFromUR(decodable, UR.fromURString(urString));
96
+ }
97
+
46
98
  /**
47
99
  * Helper function to check if an object implements URDecodable.
48
100
  */
@@ -1,4 +1,12 @@
1
- import type { UR } from "./ur.js";
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
7
+ import type { CborTaggedEncodable } from "@bcts/dcbor";
8
+
9
+ import { UR } from "./ur.js";
2
10
 
3
11
  /**
4
12
  * A type that can be encoded to a UR (Uniform Resource).
@@ -6,17 +14,27 @@ import type { UR } from "./ur.js";
6
14
  * Types implementing this interface should be able to convert themselves
7
15
  * to CBOR data and associate that with a UR type identifier.
8
16
  *
17
+ * Mirrors Rust's `UREncodable` trait (`bc-ur-rust/src/ur_encodable.rs`),
18
+ * which has a blanket impl `impl<T> UREncodable for T where T:
19
+ * CBORTaggedEncodable`. TypeScript has no equivalent of blanket impls, so
20
+ * implementers either write `ur()` / `urString()` directly *or* — for a
21
+ * type that already implements `CborTaggedEncodable` — call the helper
22
+ * functions {@link urFromEncodable} / {@link urStringFromEncodable} below
23
+ * to get the same auto-derivation that Rust provides for free.
24
+ *
9
25
  * @example
10
26
  * ```typescript
11
- * class MyType implements UREncodable {
12
- * toCBOR(): CBOR {
13
- * // Convert to CBOR
27
+ * class MyType implements UREncodable, CborTaggedEncodable {
28
+ * cborTags(): Tag[] {
29
+ * return [createTag(40000, "mytype")];
14
30
  * }
15
31
  *
16
- * ur(): UR {
17
- * const cbor = this.toCBOR();
18
- * return UR.new('mytype', cbor);
19
- * }
32
+ * untaggedCbor(): Cbor { ... }
33
+ * taggedCbor(): Cbor { return createTaggedCbor(this); }
34
+ *
35
+ * // Auto-derived from the first cbor tag's name, just like Rust.
36
+ * ur(): UR { return urFromEncodable(this); }
37
+ * urString(): string { return urStringFromEncodable(this); }
20
38
  * }
21
39
  * ```
22
40
  */
@@ -32,6 +50,40 @@ export interface UREncodable {
32
50
  urString(): string;
33
51
  }
34
52
 
53
+ /**
54
+ * Concrete equivalent of Rust's default `UREncodable::ur` impl
55
+ * (`bc-ur-rust/src/ur_encodable.rs:8-18`):
56
+ *
57
+ * - Reads the first tag returned by `encodable.cborTags()`.
58
+ * - Uses that tag's `name` as the UR type, throwing if no name is set —
59
+ * matching Rust's `panic!("CBOR tag {} must have a name. Did you call
60
+ * `register_tags()`?", tag.value())`.
61
+ * - Wraps the encodable's `untaggedCbor()` in a fresh {@link UR} bound to
62
+ * that type.
63
+ *
64
+ * Use from a class implementing both `UREncodable` and
65
+ * `CborTaggedEncodable` to skip writing the boilerplate yourself.
66
+ */
67
+ export function urFromEncodable(encodable: CborTaggedEncodable): UR {
68
+ const tags = encodable.cborTags();
69
+ const tag = tags[0];
70
+ if (tag === undefined) {
71
+ throw new Error("UREncodable: cborTags() returned no tags");
72
+ }
73
+ if (tag.name === undefined) {
74
+ throw new Error(`CBOR tag ${tag.value} must have a name. Did you call register_tags()?`);
75
+ }
76
+ return UR.new(tag.name, encodable.untaggedCbor());
77
+ }
78
+
79
+ /**
80
+ * Concrete equivalent of Rust's default `UREncodable::ur_string` impl
81
+ * (`bc-ur-rust/src/ur_encodable.rs:21`): `self.ur().string()`.
82
+ */
83
+ export function urStringFromEncodable(encodable: CborTaggedEncodable): string {
84
+ return urFromEncodable(encodable).string();
85
+ }
86
+
35
87
  /**
36
88
  * Helper function to check if an object implements UREncodable.
37
89
  */
package/src/ur-type.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import { InvalidTypeError } from "./error";
2
8
  import { isValidURType } from "./utils";
3
9
 
@@ -72,16 +78,36 @@ export class URType {
72
78
  }
73
79
 
74
80
  /**
75
- * Safely creates a URType, returning an error if invalid.
81
+ * Safely creates a URType, returning a typed `Result`-shaped
82
+ * discriminated union instead of throwing.
83
+ *
84
+ * Mirrors Rust `impl TryFrom<&str> for URType` /
85
+ * `impl TryFrom<String> for URType` (`bc-ur-rust/src/ur_type.rs`),
86
+ * which return `Result<URType, Error>`. The TS shape is the
87
+ * idiomatic discriminated form so callers can branch on `ok`
88
+ * without `instanceof`:
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const r = URType.tryFrom("test");
93
+ * if (r.ok) {
94
+ * console.log(r.value.string()); // "test"
95
+ * } else {
96
+ * console.error(r.error.message);
97
+ * }
98
+ * ```
76
99
  *
77
100
  * @param value - The UR type string
78
- * @returns Either a URType or an error
101
+ * @returns A typed Result: `{ ok: true; value: URType }` on success,
102
+ * `{ ok: false; error: InvalidTypeError }` on failure.
79
103
  */
80
- static tryFrom(value: string): URType | InvalidTypeError {
104
+ static tryFrom(
105
+ value: string,
106
+ ): { ok: true; value: URType } | { ok: false; error: InvalidTypeError } {
81
107
  try {
82
- return new URType(value);
108
+ return { ok: true, value: new URType(value) };
83
109
  } catch (error) {
84
- return error as InvalidTypeError;
110
+ return { ok: false, error: error as InvalidTypeError };
85
111
  }
86
112
  }
87
113
  }
package/src/ur.ts CHANGED
@@ -1,6 +1,20 @@
1
+ /**
2
+ * Copyright © 2023-2026 Blockchain Commons, LLC
3
+ * Copyright © 2025-2026 Parity Technologies
4
+ *
5
+ */
6
+
1
7
  import type { Cbor } from "@bcts/dcbor";
2
8
  import { decodeCbor } from "@bcts/dcbor";
3
- import { InvalidSchemeError, TypeUnspecifiedError, UnexpectedTypeError, URError } from "./error.js";
9
+ import {
10
+ BytewordsError,
11
+ CBORError,
12
+ InvalidSchemeError,
13
+ NotSinglePartError,
14
+ TypeUnspecifiedError,
15
+ UnexpectedTypeError,
16
+ URDecodeError,
17
+ } from "./error.js";
4
18
  import { URType } from "./ur-type.js";
5
19
  import { encodeBytewords, decodeBytewords, BytewordsStyle } from "./utils.js";
6
20
 
@@ -51,11 +65,26 @@ export class UR {
51
65
  /**
52
66
  * Creates a new UR from a UR string.
53
67
  *
68
+ * Mirrors Rust's `UR::from_ur_string` (`bc-ur-rust/src/ur.rs:25-38`):
69
+ * 1. lowercase the entire string.
70
+ * 2. strip the `"ur:"` prefix → {@link InvalidSchemeError} if absent.
71
+ * 3. split on the first `/` → {@link TypeUnspecifiedError} if absent.
72
+ * 4. validate the type via {@link URType} → {@link InvalidTypeError}.
73
+ * 5. delegate the data section to the upstream-style decoder, which
74
+ * classifies the UR as single- or multi-part. Multi-part input is
75
+ * rejected with {@link NotSinglePartError}.
76
+ * 6. decode the bytewords payload (CRC32 + minimal mapping) →
77
+ * {@link BytewordsError} on failure.
78
+ * 7. parse the resulting bytes as CBOR → {@link CBORError} on failure.
79
+ *
54
80
  * @param urString - A UR string like "ur:test/..."
55
81
  * @throws {InvalidSchemeError} If the string doesn't start with "ur:"
56
- * @throws {TypeUnspecifiedError} If no type is specified
82
+ * @throws {TypeUnspecifiedError} If no `/` separator is present
83
+ * @throws {InvalidTypeError} If the type contains invalid characters
57
84
  * @throws {NotSinglePartError} If the UR is multi-part
58
- * @throws {URError} If decoding fails
85
+ * @throws {URDecodeError} For upstream-decoder errors (invalid indices, etc.)
86
+ * @throws {BytewordsError} If bytewords decoding fails
87
+ * @throws {CBORError} If CBOR parsing fails
59
88
  *
60
89
  * @example
61
90
  * ```typescript
@@ -63,13 +92,8 @@ export class UR {
63
92
  * ```
64
93
  */
65
94
  static fromURString(urString: string): UR {
66
- // Decode the UR string to get the type and CBOR data
67
- const decodedUR = URStringDecoder.decode(urString);
68
- if (decodedUR === null || decodedUR === undefined) {
69
- throw new URError("Failed to decode UR string");
70
- }
71
- const { urType, cbor } = decodedUR;
72
- return new UR(new URType(urType), cbor);
95
+ const { urType, cbor } = URStringDecoder.decode(urString);
96
+ return new UR(urType, cbor);
73
97
  }
74
98
 
75
99
  private constructor(urType: URType, cbor: Cbor) {
@@ -121,15 +145,15 @@ export class UR {
121
145
 
122
146
  /**
123
147
  * Returns the QR data as bytes (uppercase UR string as UTF-8).
148
+ *
149
+ * Mirrors Rust's `UR::qr_data` (`ur.rs:52`) which does
150
+ * `self.qr_string().as_bytes().to_vec()` — the string's UTF-8 byte
151
+ * representation. We use `TextEncoder` rather than per-codepoint
152
+ * truncation so the behaviour stays correct if the QR string ever
153
+ * contains non-ASCII characters.
124
154
  */
125
155
  qrData(): Uint8Array {
126
- // Use a helper to convert string to bytes across platforms
127
- const str = this.qrString();
128
- const bytes = new Uint8Array(str.length);
129
- for (let i = 0; i < str.length; i++) {
130
- bytes[i] = str.charCodeAt(i);
131
- }
132
- return bytes;
156
+ return new TextEncoder().encode(this.qrString());
133
157
  }
134
158
 
135
159
  /**
@@ -154,12 +178,22 @@ export class UR {
154
178
 
155
179
  /**
156
180
  * Checks equality with another UR.
181
+ *
182
+ * Mirrors Rust's derived `PartialEq for UR` which compares the inner
183
+ * `ur_type` and the inner `cbor` field directly. We compare CBOR
184
+ * bytewise — `Uint8Array` equality, not `Array#toString` (which would
185
+ * coerce to a comma-joined string and could collide on pathological
186
+ * inputs).
157
187
  */
158
188
  equals(other: UR): boolean {
159
- return (
160
- this._urType.equals(other._urType) &&
161
- this._cbor.toData().toString() === other._cbor.toData().toString()
162
- );
189
+ if (!this._urType.equals(other._urType)) return false;
190
+ const a = this._cbor.toData();
191
+ const b = other._cbor.toData();
192
+ if (a.length !== b.length) return false;
193
+ for (let i = 0; i < a.length; i++) {
194
+ if (a[i] !== b[i]) return false;
195
+ }
196
+ return true;
163
197
  }
164
198
  }
165
199
 
@@ -177,39 +211,95 @@ class URStringEncoder {
177
211
 
178
212
  /**
179
213
  * Decodes a UR string back to its components.
214
+ *
215
+ * Mirrors the validation pipeline of Rust's `UR::from_ur_string`
216
+ * (`bc-ur-rust/src/ur.rs:25-38`) plus the upstream `ur::decode`
217
+ * (`ur-0.4.1/src/ur.rs:238-266`):
218
+ *
219
+ * 1. lowercase
220
+ * 2. strip `"ur:"` → {@link InvalidSchemeError}
221
+ * 3. find `/` → {@link TypeUnspecifiedError}
222
+ * 4. validate the type → {@link InvalidTypeError}
223
+ * 5. classify single- vs multi-part by looking at the data section
224
+ * 6. multi-part → {@link NotSinglePartError}
225
+ * 7. invalid multi-part indices → {@link URDecodeError("Invalid indices")}
226
+ * 8. minimal bytewords decode → {@link BytewordsError}
227
+ * 9. CBOR parse → {@link CBORError}
180
228
  */
181
229
  class URStringDecoder {
182
- static decode(urString: string): { urType: string; cbor: Cbor } | null {
230
+ static decode(urString: string): { urType: URType; cbor: Cbor } {
183
231
  const lowercased = urString.toLowerCase();
184
232
 
185
- // Check scheme
233
+ // Step 2: strip the scheme.
186
234
  if (!lowercased.startsWith("ur:")) {
187
235
  throw new InvalidSchemeError();
188
236
  }
189
-
190
- // Strip scheme
191
237
  const afterScheme = lowercased.substring(3);
192
238
 
193
- // Split into type and data
194
- const [urType, ...dataParts] = afterScheme.split("/");
195
-
196
- if (urType === "" || urType === undefined) {
239
+ // Step 3: locate the first `/`. Matches Rust's `split_once('/')`.
240
+ const slashIdx = afterScheme.indexOf("/");
241
+ if (slashIdx === -1) {
197
242
  throw new TypeUnspecifiedError();
198
243
  }
244
+ const typeStr = afterScheme.substring(0, slashIdx);
245
+ const dataSection = afterScheme.substring(slashIdx + 1);
199
246
 
200
- const data = dataParts.join("/");
201
- if (data === "" || data === undefined) {
202
- throw new TypeUnspecifiedError();
247
+ // Step 4: validate the type *before* any bytewords/CBOR work, so that a
248
+ // malformed payload with an invalid type surfaces InvalidTypeError
249
+ // rather than a downstream BytewordsError. Mirrors `ur.rs:31`.
250
+ const urType = new URType(typeStr);
251
+
252
+ // Step 5/6/7: classify the data section, the way `ur::decode` does
253
+ // (`ur-0.4.1/src/ur.rs:249-265`). If the data section contains a `/`,
254
+ // the prefix is the multi-part `<seqNum>-<seqLen>` indices.
255
+ const lastSlash = dataSection.lastIndexOf("/");
256
+ if (lastSlash !== -1) {
257
+ const indices = dataSection.substring(0, lastSlash);
258
+ const dashIdx = indices.indexOf("-");
259
+ if (dashIdx === -1) {
260
+ throw new URDecodeError("Invalid indices");
261
+ }
262
+ const seqNumStr = indices.substring(0, dashIdx);
263
+ const seqLenStr = indices.substring(dashIdx + 1);
264
+ // Rust uses `parse::<u16>()`; accept non-empty strings of digits only.
265
+ // (parseInt on `"1a"` returns 1, so we have to be strict about chars.)
266
+ if (!/^\d+$/.test(seqNumStr) || !/^\d+$/.test(seqLenStr)) {
267
+ throw new URDecodeError("Invalid indices");
268
+ }
269
+ const seqNum = Number(seqNumStr);
270
+ const seqLen = Number(seqLenStr);
271
+ if (seqNum > 0xffff || seqLen > 0xffff) {
272
+ throw new URDecodeError("Invalid indices");
273
+ }
274
+ // Successfully parsed as multi-part — but `from_ur_string` only
275
+ // accepts single-part input. Mirrors `ur.rs:33-35`.
276
+ throw new NotSinglePartError();
203
277
  }
204
278
 
279
+ // Step 8: minimal bytewords decode (CRC32 + minimal mapping).
280
+ let cborData: Uint8Array;
205
281
  try {
206
- // Decode the bytewords-encoded data (validates CRC32 checksum)
207
- const cborData = decodeBytewords(data, BytewordsStyle.Minimal);
208
- const cbor = decodeCbor(cborData);
209
- return { urType, cbor };
282
+ cborData = decodeBytewords(dataSection, BytewordsStyle.Minimal);
210
283
  } catch (error) {
211
- const errorMessage = error instanceof Error ? error.message : String(error);
212
- throw new URError(`Failed to decode UR: ${errorMessage}`);
284
+ if (error instanceof BytewordsError) {
285
+ throw error;
286
+ }
287
+ const message = error instanceof Error ? error.message : String(error);
288
+ throw new BytewordsError(message);
213
289
  }
290
+
291
+ // Step 9: CBOR parse.
292
+ let cbor: Cbor;
293
+ try {
294
+ cbor = decodeCbor(cborData);
295
+ } catch (error) {
296
+ if (error instanceof CBORError) {
297
+ throw error;
298
+ }
299
+ const message = error instanceof Error ? error.message : String(error);
300
+ throw new CBORError(message);
301
+ }
302
+
303
+ return { urType, cbor };
214
304
  }
215
305
  }