@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/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bcts/uniform-resources",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blockchain Commons Uniform Resources (UR) for TypeScript",
|
|
6
6
|
"license": "BSD-2-Clause-Patent",
|
|
7
|
-
"author": "
|
|
8
|
-
"homepage": "https://
|
|
7
|
+
"author": "Parity Technologies <admin@parity.io> (https://parity.io)",
|
|
8
|
+
"homepage": "https://bcts.dev",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/paritytech/bcts",
|
|
12
12
|
"directory": "packages/uniform-resources"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
15
|
+
"url": "https://github.com/paritytech/bcts/issues"
|
|
16
16
|
},
|
|
17
17
|
"main": "dist/index.cjs",
|
|
18
18
|
"module": "dist/index.mjs",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|
|
36
|
-
"dev": "tsdown --watch",
|
|
37
36
|
"test": "vitest run",
|
|
38
37
|
"test:watch": "vitest",
|
|
39
|
-
"lint": "eslint 'src/**/*.ts'",
|
|
40
|
-
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
38
|
+
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
|
|
39
|
+
"lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
|
|
41
40
|
"typecheck": "tsc --noEmit",
|
|
42
41
|
"clean": "rm -rf dist",
|
|
43
|
-
"docs": "typedoc"
|
|
42
|
+
"docs": "typedoc",
|
|
43
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
46
46
|
"uniform-resources",
|
|
@@ -55,20 +55,21 @@
|
|
|
55
55
|
"node": ">=18.0.0"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@bcts/
|
|
58
|
+
"@bcts/crypto": "^1.0.0-beta.1",
|
|
59
|
+
"@bcts/dcbor": "^1.0.0-beta.1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@bcts/eslint": "^0.1.0",
|
|
62
63
|
"@bcts/tsconfig": "^0.1.0",
|
|
63
|
-
"@eslint/js": "^
|
|
64
|
-
"@types/node": "^
|
|
64
|
+
"@eslint/js": "^10.0.1",
|
|
65
|
+
"@types/node": "^25.9.1",
|
|
65
66
|
"@types/pako": "^2.0.4",
|
|
66
|
-
"eslint": "^
|
|
67
|
-
"prettier": "^3.
|
|
67
|
+
"eslint": "^10.4.0",
|
|
68
|
+
"prettier": "^3.8.3",
|
|
68
69
|
"ts-node": "^10.9.2",
|
|
69
|
-
"tsdown": "^0.
|
|
70
|
-
"typedoc": "^0.28.
|
|
71
|
-
"typescript": "^
|
|
72
|
-
"vitest": "^
|
|
70
|
+
"tsdown": "^0.22.0",
|
|
71
|
+
"typedoc": "^0.28.19",
|
|
72
|
+
"typescript": "^6.0.3",
|
|
73
|
+
"vitest": "^4.1.7"
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* Namespace-style re-export of the bytewords helpers in `./utils`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors Rust's `bc_ur::bytewords` module (`bc-ur-rust/src/bytewords.rs`)
|
|
9
|
+
* so that callers can write
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { bytewords } from "@bcts/uniform-resources";
|
|
13
|
+
* bytewords.encode(data, bytewords.Style.Minimal);
|
|
14
|
+
* bytewords.identifier(fourByteSlice);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* matching the ergonomics of
|
|
18
|
+
*
|
|
19
|
+
* ```rust
|
|
20
|
+
* use bc_ur::bytewords;
|
|
21
|
+
* bytewords::encode(data, bytewords::Style::Minimal);
|
|
22
|
+
* bytewords::identifier(four_byte_slice);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* This is purely an alias module — every symbol below is the same
|
|
26
|
+
* function/value already exported individually from the package root.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
BYTEWORDS,
|
|
31
|
+
BYTEMOJIS,
|
|
32
|
+
BytewordsStyle as Style,
|
|
33
|
+
encodeBytewords as encode,
|
|
34
|
+
decodeBytewords as decode,
|
|
35
|
+
encodeBytewordsIdentifier as identifier,
|
|
36
|
+
encodeBytemojisIdentifier as bytemojiIdentifier,
|
|
37
|
+
encodeToWords,
|
|
38
|
+
encodeToBytemojis,
|
|
39
|
+
encodeToMinimalBytewords,
|
|
40
|
+
isValidBytemoji,
|
|
41
|
+
canonicalizeByteword,
|
|
42
|
+
} from "./utils";
|
package/src/error.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*
|
|
2
6
|
* Error type for UR encoding/decoding operations.
|
|
3
7
|
*/
|
|
4
8
|
export class URError extends Error {
|
|
@@ -10,30 +14,36 @@ export class URError extends Error {
|
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Error type for invalid UR schemes.
|
|
17
|
+
*
|
|
18
|
+
* Message matches Rust bc-ur-rust/src/error.rs: `invalid UR scheme`.
|
|
13
19
|
*/
|
|
14
20
|
export class InvalidSchemeError extends URError {
|
|
15
21
|
constructor() {
|
|
16
|
-
super("
|
|
22
|
+
super("invalid UR scheme");
|
|
17
23
|
this.name = "InvalidSchemeError";
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* Error type for unspecified UR types.
|
|
29
|
+
*
|
|
30
|
+
* Message matches Rust bc-ur-rust/src/error.rs: `no UR type specified`.
|
|
23
31
|
*/
|
|
24
32
|
export class TypeUnspecifiedError extends URError {
|
|
25
33
|
constructor() {
|
|
26
|
-
super("
|
|
34
|
+
super("no UR type specified");
|
|
27
35
|
this.name = "TypeUnspecifiedError";
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
40
|
* Error type for invalid UR types.
|
|
41
|
+
*
|
|
42
|
+
* Message matches Rust bc-ur-rust/src/error.rs: `invalid UR type`.
|
|
33
43
|
*/
|
|
34
44
|
export class InvalidTypeError extends URError {
|
|
35
45
|
constructor() {
|
|
36
|
-
super("
|
|
46
|
+
super("invalid UR type");
|
|
37
47
|
this.name = "InvalidTypeError";
|
|
38
48
|
}
|
|
39
49
|
}
|
|
@@ -50,34 +60,52 @@ export class NotSinglePartError extends URError {
|
|
|
50
60
|
|
|
51
61
|
/**
|
|
52
62
|
* Error type for unexpected UR types.
|
|
63
|
+
*
|
|
64
|
+
* Message matches Rust bc-ur-rust/src/error.rs:
|
|
65
|
+
* `expected UR type {expected}, but found {found}`.
|
|
53
66
|
*/
|
|
54
67
|
export class UnexpectedTypeError extends URError {
|
|
55
68
|
constructor(expected: string, found: string) {
|
|
56
|
-
super(`
|
|
69
|
+
super(`expected UR type ${expected}, but found ${found}`);
|
|
57
70
|
this.name = "UnexpectedTypeError";
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/**
|
|
62
75
|
* Error type for Bytewords encoding/decoding errors.
|
|
76
|
+
*
|
|
77
|
+
* Message matches Rust bc-ur-rust/src/error.rs: `Bytewords error ({0})`.
|
|
63
78
|
*/
|
|
64
79
|
export class BytewordsError extends URError {
|
|
65
80
|
constructor(message: string) {
|
|
66
|
-
super(`Bytewords error
|
|
81
|
+
super(`Bytewords error (${message})`);
|
|
67
82
|
this.name = "BytewordsError";
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
/**
|
|
72
87
|
* Error type for CBOR encoding/decoding errors.
|
|
88
|
+
*
|
|
89
|
+
* Message matches Rust bc-ur-rust/src/error.rs: `CBOR error ({0})`.
|
|
73
90
|
*/
|
|
74
91
|
export class CBORError extends URError {
|
|
75
92
|
constructor(message: string) {
|
|
76
|
-
super(`CBOR error
|
|
93
|
+
super(`CBOR error (${message})`);
|
|
77
94
|
this.name = "CBORError";
|
|
78
95
|
}
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Error type for UR decoder errors.
|
|
100
|
+
* Matches Rust's Error::UR(String) variant.
|
|
101
|
+
*/
|
|
102
|
+
export class URDecodeError extends URError {
|
|
103
|
+
constructor(message: string) {
|
|
104
|
+
super(`UR decoder error (${message})`);
|
|
105
|
+
this.name = "URDecodeError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
export type Result<T> = T | Error;
|
|
82
110
|
|
|
83
111
|
/**
|
package/src/fountain.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*
|
|
2
6
|
* Fountain code implementation for multipart URs.
|
|
3
7
|
*
|
|
4
8
|
* This implements a hybrid fixed-rate and rateless fountain code system
|
|
@@ -30,27 +34,86 @@ export interface FountainPart {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
|
-
*
|
|
37
|
+
* Calculates the quotient of `a` and `b`, rounded toward positive infinity.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors Rust `ur-0.4.1/src/fountain.rs::div_ceil`.
|
|
34
40
|
*/
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const start = i * fragmentLen;
|
|
41
|
-
const end = Math.min(start + fragmentLen, message.length);
|
|
42
|
-
const fragment = new Uint8Array(fragmentLen);
|
|
41
|
+
function divCeil(a: number, b: number): number {
|
|
42
|
+
const d = Math.floor(a / b);
|
|
43
|
+
const r = a % b;
|
|
44
|
+
return r > 0 ? d + 1 : d;
|
|
45
|
+
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Computes the optimal fragment length for a given message length and
|
|
49
|
+
* maximum fragment length.
|
|
50
|
+
*
|
|
51
|
+
* The algorithm:
|
|
52
|
+
* fragment_count = ceil(data_length / max_fragment_length)
|
|
53
|
+
* fragment_length = ceil(data_length / fragment_count)
|
|
54
|
+
*
|
|
55
|
+
* This produces fragments that are as balanced as possible while still
|
|
56
|
+
* respecting `maxFragmentLen` as an upper bound on each fragment. For
|
|
57
|
+
* example, a 10-byte message with `maxFragmentLen = 6` yields a fragment
|
|
58
|
+
* length of 5 (so two even 5-byte fragments) rather than 6 (one full
|
|
59
|
+
* fragment plus a 4-byte tail).
|
|
60
|
+
*
|
|
61
|
+
* Mirrors Rust `ur-0.4.1/src/fountain.rs::fragment_length` byte-for-byte.
|
|
62
|
+
*/
|
|
63
|
+
export function fragmentLength(dataLength: number, maxFragmentLength: number): number {
|
|
64
|
+
const fragmentCount = divCeil(dataLength, maxFragmentLength);
|
|
65
|
+
return divCeil(dataLength, fragmentCount);
|
|
66
|
+
}
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Splits `data` into a list of `fragmentLen`-sized chunks, zero-padding
|
|
70
|
+
* the last chunk if necessary so that every chunk is exactly
|
|
71
|
+
* `fragmentLen` bytes long.
|
|
72
|
+
*
|
|
73
|
+
* Note: `fragmentLen` is the **already-computed** fragment length (see
|
|
74
|
+
* {@link fragmentLength}), not the user-facing maximum fragment length.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors Rust `ur-0.4.1/src/fountain.rs::partition` byte-for-byte.
|
|
77
|
+
*/
|
|
78
|
+
export function partition(data: Uint8Array, fragmentLen: number): Uint8Array[] {
|
|
79
|
+
if (fragmentLen < 1) {
|
|
80
|
+
throw new Error("fragment length must be at least 1");
|
|
49
81
|
}
|
|
82
|
+
const remainder = data.length % fragmentLen;
|
|
83
|
+
const padding = remainder === 0 ? 0 : fragmentLen - remainder;
|
|
84
|
+
const padded = new Uint8Array(data.length + padding);
|
|
85
|
+
padded.set(data);
|
|
86
|
+
// Trailing bytes are already zero by Uint8Array's default initialization.
|
|
50
87
|
|
|
88
|
+
const fragments: Uint8Array[] = [];
|
|
89
|
+
for (let start = 0; start < padded.length; start += fragmentLen) {
|
|
90
|
+
fragments.push(padded.slice(start, start + fragmentLen));
|
|
91
|
+
}
|
|
51
92
|
return fragments;
|
|
52
93
|
}
|
|
53
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Convenience: compute the optimal fragment length for `message` given
|
|
97
|
+
* `maxFragmentLen` and partition the message into that many fragments.
|
|
98
|
+
*
|
|
99
|
+
* Equivalent to:
|
|
100
|
+
* ```ts
|
|
101
|
+
* partition(message, fragmentLength(message.length, maxFragmentLen))
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* This is what {@link FountainEncoder} does internally when constructing
|
|
105
|
+
* its fragment table.
|
|
106
|
+
*/
|
|
107
|
+
export function splitMessage(message: Uint8Array, maxFragmentLen: number): Uint8Array[] {
|
|
108
|
+
if (maxFragmentLen < 1) {
|
|
109
|
+
throw new Error("max fragment length must be at least 1");
|
|
110
|
+
}
|
|
111
|
+
if (message.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return partition(message, fragmentLength(message.length, maxFragmentLen));
|
|
115
|
+
}
|
|
116
|
+
|
|
54
117
|
/**
|
|
55
118
|
* XOR two Uint8Arrays together.
|
|
56
119
|
*/
|
|
@@ -71,6 +134,11 @@ export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
71
134
|
* This uses a seeded Xoshiro256** PRNG to deterministically select fragments,
|
|
72
135
|
* ensuring encoder and decoder agree without explicit coordination.
|
|
73
136
|
*
|
|
137
|
+
* The algorithm matches the BC-UR reference implementation:
|
|
138
|
+
* 1. For pure parts (seqNum <= seqLen), return single fragment index
|
|
139
|
+
* 2. For mixed parts, use weighted sampling to choose degree
|
|
140
|
+
* 3. Shuffle all indices and take the first 'degree' indices
|
|
141
|
+
*
|
|
74
142
|
* @param seqNum - The sequence number (1-based)
|
|
75
143
|
* @param seqLen - Total number of pure fragments
|
|
76
144
|
* @param checksum - CRC32 checksum of the message
|
|
@@ -86,43 +154,18 @@ export function chooseFragments(seqNum: number, seqLen: number, checksum: number
|
|
|
86
154
|
const seed = createSeed(checksum, seqNum);
|
|
87
155
|
const rng = new Xoshiro256(seed);
|
|
88
156
|
|
|
89
|
-
// Choose degree
|
|
90
|
-
|
|
91
|
-
const degree = chooseDegree(rng, seqLen);
|
|
157
|
+
// Choose degree using weighted sampler (1/k distribution)
|
|
158
|
+
const degree = rng.chooseDegree(seqLen);
|
|
92
159
|
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
indices.add(index);
|
|
160
|
+
// Create array of all indices [0, 1, 2, ..., seqLen-1]
|
|
161
|
+
const allIndices: number[] = [];
|
|
162
|
+
for (let i = 0; i < seqLen; i++) {
|
|
163
|
+
allIndices.push(i);
|
|
98
164
|
}
|
|
99
165
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Chooses the degree (number of fragments to mix) using a simplified
|
|
105
|
-
* robust soliton distribution.
|
|
106
|
-
*
|
|
107
|
-
* This ensures good coverage of fragments for efficient decoding.
|
|
108
|
-
*/
|
|
109
|
-
function chooseDegree(rng: Xoshiro256, seqLen: number): number {
|
|
110
|
-
// Use a simplified distribution that tends toward lower degrees
|
|
111
|
-
// but can occasionally include more fragments
|
|
112
|
-
const r = rng.nextDouble();
|
|
113
|
-
|
|
114
|
-
// Probability distribution favoring lower degrees
|
|
115
|
-
// Based on robust soliton distribution
|
|
116
|
-
if (r < 0.5) {
|
|
117
|
-
return 1;
|
|
118
|
-
} else if (r < 0.75) {
|
|
119
|
-
return 2;
|
|
120
|
-
} else if (r < 0.9) {
|
|
121
|
-
return Math.min(3, seqLen);
|
|
122
|
-
} else {
|
|
123
|
-
// Higher degrees are less common but help with convergence
|
|
124
|
-
return Math.min(rng.nextInt(4, seqLen + 1), seqLen);
|
|
125
|
-
}
|
|
166
|
+
// Shuffle all indices and take the first 'degree' indices
|
|
167
|
+
const shuffled = rng.shuffled(allIndices);
|
|
168
|
+
return shuffled.slice(0, degree);
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
/**
|
|
@@ -160,15 +203,25 @@ export class FountainEncoder {
|
|
|
160
203
|
*
|
|
161
204
|
* @param message - The message to encode
|
|
162
205
|
* @param maxFragmentLen - Maximum length of each fragment
|
|
206
|
+
*
|
|
207
|
+
* @throws if `message` is empty (mirrors Rust `Error::EmptyMessage`).
|
|
208
|
+
* @throws if `maxFragmentLen < 1` (mirrors Rust `Error::InvalidFragmentLen`).
|
|
163
209
|
*/
|
|
164
210
|
constructor(message: Uint8Array, maxFragmentLen: number) {
|
|
211
|
+
if (message.length === 0) {
|
|
212
|
+
throw new Error("expected non-empty message");
|
|
213
|
+
}
|
|
165
214
|
if (maxFragmentLen < 1) {
|
|
166
|
-
throw new Error("
|
|
215
|
+
throw new Error("expected positive maximum fragment length");
|
|
167
216
|
}
|
|
168
217
|
|
|
169
218
|
this.messageLen = message.length;
|
|
170
219
|
this.checksum = crc32(message);
|
|
171
|
-
|
|
220
|
+
// Mirrors Rust `Encoder::new`:
|
|
221
|
+
// let fragment_length = fragment_length(message.len(), max_fragment_length);
|
|
222
|
+
// let fragments = partition(message.to_vec(), fragment_length);
|
|
223
|
+
const optimalLen = fragmentLength(message.length, maxFragmentLen);
|
|
224
|
+
this.fragments = partition(message, optimalLen);
|
|
172
225
|
}
|
|
173
226
|
|
|
174
227
|
/**
|
|
@@ -232,54 +285,86 @@ export class FountainDecoder {
|
|
|
232
285
|
private seqLen: number | null = null;
|
|
233
286
|
private messageLen: number | null = null;
|
|
234
287
|
private checksum: number | null = null;
|
|
288
|
+
private fragmentLen: number | null = null;
|
|
235
289
|
|
|
236
290
|
// Storage for received data
|
|
237
291
|
private readonly pureFragments = new Map<number, Uint8Array>();
|
|
238
292
|
private readonly mixedParts = new Map<number, { indices: number[]; data: Uint8Array }>();
|
|
293
|
+
// Set of already-received `indices` keys (joined by `,`) — Rust uses
|
|
294
|
+
// `BTreeSet<Vec<usize>>` so two parts producing the same index set are
|
|
295
|
+
// deduped even when they have different sequence numbers. Mirrors
|
|
296
|
+
// `ur-0.4.1/src/fountain.rs::Decoder.received`.
|
|
297
|
+
private readonly receivedIndexSets = new Set<string>();
|
|
239
298
|
|
|
240
299
|
/**
|
|
241
300
|
* Receives a fountain part and attempts to decode.
|
|
242
301
|
*
|
|
243
302
|
* @param part - The fountain part to receive
|
|
244
|
-
* @returns true if
|
|
303
|
+
* @returns `true` if this part contributed new information,
|
|
304
|
+
* `false` if it was an exact duplicate of a part already seen
|
|
305
|
+
* (or if the decoder was already complete).
|
|
306
|
+
*
|
|
307
|
+
* @throws if the part is empty or inconsistent with previously received
|
|
308
|
+
* parts. Mirrors Rust `Error::EmptyPart` and `Error::InconsistentPart`.
|
|
245
309
|
*/
|
|
246
310
|
receive(part: FountainPart): boolean {
|
|
311
|
+
// Mirrors Rust `Decoder::receive`:
|
|
312
|
+
// if self.complete() { return Ok(false); }
|
|
313
|
+
if (this.isComplete()) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Mirrors Rust's eager EmptyPart check.
|
|
318
|
+
if (part.seqLen === 0 || part.data.length === 0 || part.messageLen === 0) {
|
|
319
|
+
throw new Error("expected non-empty part");
|
|
320
|
+
}
|
|
321
|
+
|
|
247
322
|
// Initialize on first part
|
|
248
323
|
if (this.seqLen === null) {
|
|
249
324
|
this.seqLen = part.seqLen;
|
|
250
325
|
this.messageLen = part.messageLen;
|
|
251
326
|
this.checksum = part.checksum;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
327
|
+
this.fragmentLen = part.data.length;
|
|
328
|
+
} else if (
|
|
329
|
+
// Mirrors Rust `Decoder::validate` exactly: every metadata field
|
|
330
|
+
// (sequence_count, message_length, checksum, fragment_length) must
|
|
331
|
+
// match across all received parts.
|
|
256
332
|
part.seqLen !== this.seqLen ||
|
|
257
333
|
part.messageLen !== this.messageLen ||
|
|
258
|
-
part.checksum !== this.checksum
|
|
334
|
+
part.checksum !== this.checksum ||
|
|
335
|
+
part.data.length !== this.fragmentLen
|
|
259
336
|
) {
|
|
260
|
-
throw new Error("
|
|
337
|
+
throw new Error("part is inconsistent with previous ones");
|
|
261
338
|
}
|
|
262
339
|
|
|
263
|
-
// Determine which fragments this part contains
|
|
264
|
-
const indices = chooseFragments(part.seqNum, this.seqLen, this.checksum);
|
|
340
|
+
// Determine which fragments this part contains.
|
|
341
|
+
const indices = chooseFragments(part.seqNum, this.seqLen, this.checksum ?? 0);
|
|
342
|
+
// Rust sorts the indices implicitly via `BTreeSet` membership; we
|
|
343
|
+
// explicitly sort the key so that two parts whose `chooseFragments`
|
|
344
|
+
// output is the same multiset (regardless of order) collapse to the
|
|
345
|
+
// same dedup key. In practice `chooseFragments` already produces a
|
|
346
|
+
// deterministic shuffle, so this is just defensive.
|
|
347
|
+
const indexSetKey = [...indices].sort((a, b) => a - b).join(",");
|
|
348
|
+
if (this.receivedIndexSets.has(indexSetKey)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
this.receivedIndexSets.add(indexSetKey);
|
|
265
352
|
|
|
266
353
|
if (indices.length === 1) {
|
|
267
|
-
// Pure fragment
|
|
354
|
+
// Pure fragment (or degree-1 mixed that acts like pure).
|
|
268
355
|
const index = indices[0];
|
|
269
356
|
if (!this.pureFragments.has(index)) {
|
|
270
357
|
this.pureFragments.set(index, part.data);
|
|
271
358
|
}
|
|
272
359
|
} else {
|
|
273
|
-
// Mixed fragment - store for later reduction
|
|
274
|
-
|
|
275
|
-
this.mixedParts.set(part.seqNum, { indices, data: part.data });
|
|
276
|
-
}
|
|
360
|
+
// Mixed fragment - store for later reduction.
|
|
361
|
+
this.mixedParts.set(part.seqNum, { indices, data: part.data });
|
|
277
362
|
}
|
|
278
363
|
|
|
279
|
-
// Try to reduce mixed parts
|
|
364
|
+
// Try to reduce mixed parts.
|
|
280
365
|
this.reduceMixedParts();
|
|
281
366
|
|
|
282
|
-
return
|
|
367
|
+
return true;
|
|
283
368
|
}
|
|
284
369
|
|
|
285
370
|
/**
|
|
@@ -391,7 +476,9 @@ export class FountainDecoder {
|
|
|
391
476
|
this.seqLen = null;
|
|
392
477
|
this.messageLen = null;
|
|
393
478
|
this.checksum = null;
|
|
479
|
+
this.fragmentLen = null;
|
|
394
480
|
this.pureFragments.clear();
|
|
395
481
|
this.mixedParts.clear();
|
|
482
|
+
this.receivedIndexSets.clear();
|
|
396
483
|
}
|
|
397
484
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © 2023-2026 Blockchain Commons, LLC
|
|
3
|
+
* Copyright © 2025-2026 Parity Technologies
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
// Core types
|
|
2
8
|
export { UR } from "./ur";
|
|
3
9
|
export { URType } from "./ur-type";
|
|
4
10
|
|
|
5
|
-
// Error types
|
|
11
|
+
// Error types (matching Rust's Error enum variants)
|
|
6
12
|
export {
|
|
7
13
|
URError,
|
|
14
|
+
URDecodeError,
|
|
8
15
|
InvalidSchemeError,
|
|
9
16
|
TypeUnspecifiedError,
|
|
10
17
|
InvalidTypeError,
|
|
@@ -18,9 +25,9 @@ export {
|
|
|
18
25
|
export type { Result } from "./error";
|
|
19
26
|
|
|
20
27
|
// Traits/Interfaces
|
|
21
|
-
export { isUREncodable } from "./ur-encodable";
|
|
28
|
+
export { isUREncodable, urFromEncodable, urStringFromEncodable } from "./ur-encodable";
|
|
22
29
|
export type { UREncodable } from "./ur-encodable";
|
|
23
|
-
export { isURDecodable } from "./ur-decodable";
|
|
30
|
+
export { isURDecodable, decodableFromUR, decodableFromURString } from "./ur-decodable";
|
|
24
31
|
export type { URDecodable } from "./ur-decodable";
|
|
25
32
|
export { isURCodable } from "./ur-codable";
|
|
26
33
|
export type { URCodable } from "./ur-codable";
|
|
@@ -29,33 +36,27 @@ export type { URCodable } from "./ur-codable";
|
|
|
29
36
|
export { MultipartEncoder } from "./multipart-encoder";
|
|
30
37
|
export { MultipartDecoder } from "./multipart-decoder";
|
|
31
38
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
xorBytes,
|
|
38
|
-
chooseFragments,
|
|
39
|
-
mixFragments,
|
|
40
|
-
} from "./fountain";
|
|
41
|
-
export type { FountainPart } from "./fountain";
|
|
42
|
-
|
|
43
|
-
// PRNG for deterministic fountain code mixing
|
|
44
|
-
export { Xoshiro256, createSeed } from "./xoshiro";
|
|
45
|
-
|
|
46
|
-
// Utilities
|
|
39
|
+
// URType validation helpers (mirroring Rust's `URTypeChar` / `URTypeString`
|
|
40
|
+
// trait sugar in `bc-ur-rust/src/utils.rs`).
|
|
41
|
+
export { isURTypeChar, isValidURType, validateURType } from "./utils";
|
|
42
|
+
|
|
43
|
+
// Bytewords module (matching Rust's pub mod bytewords)
|
|
47
44
|
export {
|
|
48
|
-
isURTypeChar,
|
|
49
|
-
isValidURType,
|
|
50
|
-
validateURType,
|
|
51
45
|
BYTEWORDS,
|
|
52
|
-
BYTEWORDS_MAP,
|
|
53
46
|
BYTEMOJIS,
|
|
54
|
-
encodeBytewordsIdentifier,
|
|
55
|
-
encodeBytemojisIdentifier,
|
|
56
47
|
BytewordsStyle,
|
|
57
48
|
encodeBytewords,
|
|
58
49
|
decodeBytewords,
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
encodeBytewordsIdentifier,
|
|
51
|
+
encodeBytemojisIdentifier,
|
|
52
|
+
encodeToWords,
|
|
53
|
+
encodeToBytemojis,
|
|
54
|
+
encodeToMinimalBytewords,
|
|
55
|
+
isValidBytemoji,
|
|
56
|
+
canonicalizeByteword,
|
|
61
57
|
} from "./utils";
|
|
58
|
+
|
|
59
|
+
// Namespace-style re-export so callers can write `bytewords.encode(...)` /
|
|
60
|
+
// `bytewords.decode(...)` to mirror Rust's `bc_ur::bytewords::encode(...)` etc.
|
|
61
|
+
// Tracked in PARITY_AUDIT.md §3.1 / §4.5.
|
|
62
|
+
export * as bytewords from "./bytewords-namespace";
|