@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.
- package/LICENSE +3 -2
- package/README.md +1 -1
- package/dist/chunk-D7D4PA-g.mjs +13 -0
- package/dist/index.cjs +690 -6852
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +245 -257
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +243 -257
- package/dist/index.d.mts.map +1 -1
- package/dist/index.iife.js +748 -6913
- package/dist/index.iife.js.map +1 -1
- package/dist/index.mjs +666 -6844
- package/dist/index.mjs.map +1 -1
- package/package.json +19 -18
- package/src/bytewords-namespace.ts +42 -0
- package/src/error.ts +34 -6
- package/src/fountain.ts +152 -65
- package/src/index.ts +27 -26
- package/src/multipart-decoder.ts +36 -36
- package/src/multipart-encoder.ts +18 -54
- package/src/ur-codable.ts +6 -0
- package/src/ur-decodable.ts +60 -8
- package/src/ur-encodable.ts +60 -8
- package/src/ur-type.ts +31 -5
- package/src/ur.ts +128 -38
- package/src/utils.ts +195 -39
- package/src/xoshiro.ts +189 -77
package/src/multipart-decoder.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
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
|
|
131
|
+
// Decode bytewords to get CBOR data
|
|
132
|
+
const cborData = decodeBytewords(partInfo.encodedData, BytewordsStyle.Minimal);
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
134
|
+
// Decode the CBOR array
|
|
135
|
+
const decoded = decodeCbor(cborData);
|
|
129
136
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
141
|
-
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
|
/**
|
package/src/multipart-encoder.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
112
|
-
const
|
|
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
|
-
|
|
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
package/src/ur-decodable.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
*/
|
package/src/ur-encodable.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
13
|
-
*
|
|
27
|
+
* class MyType implements UREncodable, CborTaggedEncodable {
|
|
28
|
+
* cborTags(): Tag[] {
|
|
29
|
+
* return [createTag(40000, "mytype")];
|
|
14
30
|
* }
|
|
15
31
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
101
|
+
* @returns A typed Result: `{ ok: true; value: URType }` on success,
|
|
102
|
+
* `{ ok: false; error: InvalidTypeError }` on failure.
|
|
79
103
|
*/
|
|
80
|
-
static tryFrom(
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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:
|
|
230
|
+
static decode(urString: string): { urType: URType; cbor: Cbor } {
|
|
183
231
|
const lowercased = urString.toLowerCase();
|
|
184
232
|
|
|
185
|
-
//
|
|
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
|
-
//
|
|
194
|
-
const
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
}
|