Skip to content

Commit 947225f

Browse files
authored
feat: export the getCrc32 function from @dfinity/principal (#1077)
* refactor: export getCrc utility function from principal package * chore: update changelog
1 parent a863ce1 commit 947225f

File tree

6 files changed

+178
-174
lines changed

6 files changed

+178
-174
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
- feat: export the `getCrc32` function from `@dfinity/principal`
6+
57
## [3.0.2] - 2025-07-23
68

79
- fix: canonicalizes record and variant labels during subtype checking

packages/principal/src/index.ts

Lines changed: 2 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,2 @@
1-
import { decode, encode } from './utils/base32.ts';
2-
import { getCrc32 } from './utils/getCrc.ts';
3-
import { sha224 } from '@noble/hashes/sha2';
4-
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
5-
6-
export const JSON_KEY_PRINCIPAL = '__principal__';
7-
const SELF_AUTHENTICATING_SUFFIX = 2;
8-
const ANONYMOUS_SUFFIX = 4;
9-
10-
const MANAGEMENT_CANISTER_PRINCIPAL_TEXT_STR = 'aaaaa-aa';
11-
12-
export type JsonnablePrincipal = {
13-
[JSON_KEY_PRINCIPAL]: string;
14-
};
15-
16-
export class Principal {
17-
public static anonymous(): Principal {
18-
return new this(new Uint8Array([ANONYMOUS_SUFFIX]));
19-
}
20-
21-
/**
22-
* Utility method, returning the principal representing the management canister, decoded from the hex string `'aaaaa-aa'`
23-
* @returns {Principal} principal of the management canister
24-
*/
25-
public static managementCanister(): Principal {
26-
return this.fromText(MANAGEMENT_CANISTER_PRINCIPAL_TEXT_STR);
27-
}
28-
29-
public static selfAuthenticating(publicKey: Uint8Array): Principal {
30-
const sha = sha224(publicKey);
31-
return new this(new Uint8Array([...sha, SELF_AUTHENTICATING_SUFFIX]));
32-
}
33-
34-
public static from(other: unknown): Principal {
35-
if (typeof other === 'string') {
36-
return Principal.fromText(other);
37-
} else if (Object.getPrototypeOf(other) === Uint8Array.prototype) {
38-
return new Principal(other as Uint8Array);
39-
} else if (Principal.isPrincipal(other)) {
40-
return new Principal(other._arr);
41-
}
42-
43-
throw new Error(`Impossible to convert ${JSON.stringify(other)} to Principal.`);
44-
}
45-
46-
public static fromHex(hex: string): Principal {
47-
return new this(hexToBytes(hex));
48-
}
49-
50-
public static fromText(text: string): Principal {
51-
let maybePrincipal = text;
52-
// If formatted as JSON string, parse it first
53-
if (text.includes(JSON_KEY_PRINCIPAL)) {
54-
const obj = JSON.parse(text);
55-
if (JSON_KEY_PRINCIPAL in obj) {
56-
maybePrincipal = obj[JSON_KEY_PRINCIPAL];
57-
}
58-
}
59-
60-
const canisterIdNoDash = maybePrincipal.toLowerCase().replace(/-/g, '');
61-
62-
let arr = decode(canisterIdNoDash);
63-
arr = arr.slice(4, arr.length);
64-
65-
const principal = new this(arr);
66-
if (principal.toText() !== maybePrincipal) {
67-
throw new Error(
68-
`Principal "${principal.toText()}" does not have a valid checksum (original value "${maybePrincipal}" may not be a valid Principal ID).`,
69-
);
70-
}
71-
72-
return principal;
73-
}
74-
75-
public static fromUint8Array(arr: Uint8Array): Principal {
76-
return new this(arr);
77-
}
78-
79-
public static isPrincipal(other: unknown): other is Principal {
80-
return (
81-
other instanceof Principal ||
82-
(typeof other === 'object' &&
83-
other !== null &&
84-
'_isPrincipal' in other &&
85-
(other as { _isPrincipal: boolean })['_isPrincipal'] === true &&
86-
'_arr' in other &&
87-
(other as { _arr: Uint8Array })['_arr'] instanceof Uint8Array)
88-
);
89-
}
90-
91-
public readonly _isPrincipal = true;
92-
93-
protected constructor(private _arr: Uint8Array) {}
94-
95-
public isAnonymous(): boolean {
96-
return this._arr.byteLength === 1 && this._arr[0] === ANONYMOUS_SUFFIX;
97-
}
98-
99-
public toUint8Array(): Uint8Array {
100-
return this._arr;
101-
}
102-
103-
public toHex(): string {
104-
return bytesToHex(this._arr).toUpperCase();
105-
}
106-
107-
public toText(): string {
108-
const checksumArrayBuf = new ArrayBuffer(4);
109-
const view = new DataView(checksumArrayBuf);
110-
view.setUint32(0, getCrc32(this._arr));
111-
const checksum = new Uint8Array(checksumArrayBuf);
112-
113-
const array = new Uint8Array([...checksum, ...this._arr]);
114-
115-
const result = encode(array);
116-
const matches = result.match(/.{1,5}/g);
117-
if (!matches) {
118-
// This should only happen if there's no character, which is unreachable.
119-
throw new Error();
120-
}
121-
return matches.join('-');
122-
}
123-
124-
public toString(): string {
125-
return this.toText();
126-
}
127-
128-
/**
129-
* Serializes to JSON
130-
* @returns {JsonnablePrincipal} a JSON object with a single key, {@link JSON_KEY_PRINCIPAL}, whose value is the principal as a string
131-
*/
132-
public toJSON(): JsonnablePrincipal {
133-
return { [JSON_KEY_PRINCIPAL]: this.toText() };
134-
}
135-
136-
/**
137-
* Utility method taking a Principal to compare against. Used for determining canister ranges in certificate verification
138-
* @param {Principal} other - a {@link Principal} to compare
139-
* @returns {'lt' | 'eq' | 'gt'} `'lt' | 'eq' | 'gt'` a string, representing less than, equal to, or greater than
140-
*/
141-
public compareTo(other: Principal): 'lt' | 'eq' | 'gt' {
142-
for (let i = 0; i < Math.min(this._arr.length, other._arr.length); i++) {
143-
if (this._arr[i] < other._arr[i]) return 'lt';
144-
else if (this._arr[i] > other._arr[i]) return 'gt';
145-
}
146-
// Here, at least one principal is a prefix of the other principal (they could be the same)
147-
if (this._arr.length < other._arr.length) return 'lt';
148-
if (this._arr.length > other._arr.length) return 'gt';
149-
return 'eq';
150-
}
151-
152-
/**
153-
* Utility method checking whether a provided Principal is less than or equal to the current one using the {@link Principal.compareTo} method
154-
* @param other a {@link Principal} to compare
155-
* @returns {boolean} boolean
156-
*/
157-
public ltEq(other: Principal): boolean {
158-
const cmp = this.compareTo(other);
159-
return cmp == 'lt' || cmp == 'eq';
160-
}
161-
162-
/**
163-
* Utility method checking whether a provided Principal is greater than or equal to the current one using the {@link Principal.compareTo} method
164-
* @param other a {@link Principal} to compare
165-
* @returns {boolean} boolean
166-
*/
167-
public gtEq(other: Principal): boolean {
168-
const cmp = this.compareTo(other);
169-
return cmp == 'gt' || cmp == 'eq';
170-
}
171-
}
1+
export * from './principal.ts';
2+
export { getCrc32 } from './utils/getCrc.ts';

packages/principal/src/index.test.ts renamed to packages/principal/src/principal.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Principal } from './index.ts';
1+
import { Principal } from './principal.ts';
22
import { jsonReviver } from '@dfinity/utils';
33

44
describe('Principal', () => {
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { decode, encode } from './utils/base32.ts';
2+
import { getCrc32 } from './utils/getCrc.ts';
3+
import { sha224 } from '@noble/hashes/sha2';
4+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
5+
6+
export const JSON_KEY_PRINCIPAL = '__principal__';
7+
const SELF_AUTHENTICATING_SUFFIX = 2;
8+
const ANONYMOUS_SUFFIX = 4;
9+
10+
const MANAGEMENT_CANISTER_PRINCIPAL_TEXT_STR = 'aaaaa-aa';
11+
12+
export type JsonnablePrincipal = {
13+
[JSON_KEY_PRINCIPAL]: string;
14+
};
15+
16+
export class Principal {
17+
public static anonymous(): Principal {
18+
return new this(new Uint8Array([ANONYMOUS_SUFFIX]));
19+
}
20+
21+
/**
22+
* Utility method, returning the principal representing the management canister, decoded from the hex string `'aaaaa-aa'`
23+
* @returns {Principal} principal of the management canister
24+
*/
25+
public static managementCanister(): Principal {
26+
return this.fromText(MANAGEMENT_CANISTER_PRINCIPAL_TEXT_STR);
27+
}
28+
29+
public static selfAuthenticating(publicKey: Uint8Array): Principal {
30+
const sha = sha224(publicKey);
31+
return new this(new Uint8Array([...sha, SELF_AUTHENTICATING_SUFFIX]));
32+
}
33+
34+
public static from(other: unknown): Principal {
35+
if (typeof other === 'string') {
36+
return Principal.fromText(other);
37+
} else if (Object.getPrototypeOf(other) === Uint8Array.prototype) {
38+
return new Principal(other as Uint8Array);
39+
} else if (Principal.isPrincipal(other)) {
40+
return new Principal(other._arr);
41+
}
42+
43+
throw new Error(`Impossible to convert ${JSON.stringify(other)} to Principal.`);
44+
}
45+
46+
public static fromHex(hex: string): Principal {
47+
return new this(hexToBytes(hex));
48+
}
49+
50+
public static fromText(text: string): Principal {
51+
let maybePrincipal = text;
52+
// If formatted as JSON string, parse it first
53+
if (text.includes(JSON_KEY_PRINCIPAL)) {
54+
const obj = JSON.parse(text);
55+
if (JSON_KEY_PRINCIPAL in obj) {
56+
maybePrincipal = obj[JSON_KEY_PRINCIPAL];
57+
}
58+
}
59+
60+
const canisterIdNoDash = maybePrincipal.toLowerCase().replace(/-/g, '');
61+
62+
let arr = decode(canisterIdNoDash);
63+
arr = arr.slice(4, arr.length);
64+
65+
const principal = new this(arr);
66+
if (principal.toText() !== maybePrincipal) {
67+
throw new Error(
68+
`Principal "${principal.toText()}" does not have a valid checksum (original value "${maybePrincipal}" may not be a valid Principal ID).`,
69+
);
70+
}
71+
72+
return principal;
73+
}
74+
75+
public static fromUint8Array(arr: Uint8Array): Principal {
76+
return new this(arr);
77+
}
78+
79+
public static isPrincipal(other: unknown): other is Principal {
80+
return (
81+
other instanceof Principal ||
82+
(typeof other === 'object' &&
83+
other !== null &&
84+
'_isPrincipal' in other &&
85+
(other as { _isPrincipal: boolean })['_isPrincipal'] === true &&
86+
'_arr' in other &&
87+
(other as { _arr: Uint8Array })['_arr'] instanceof Uint8Array)
88+
);
89+
}
90+
91+
public readonly _isPrincipal = true;
92+
93+
protected constructor(private _arr: Uint8Array) {}
94+
95+
public isAnonymous(): boolean {
96+
return this._arr.byteLength === 1 && this._arr[0] === ANONYMOUS_SUFFIX;
97+
}
98+
99+
public toUint8Array(): Uint8Array {
100+
return this._arr;
101+
}
102+
103+
public toHex(): string {
104+
return bytesToHex(this._arr).toUpperCase();
105+
}
106+
107+
public toText(): string {
108+
const checksumArrayBuf = new ArrayBuffer(4);
109+
const view = new DataView(checksumArrayBuf);
110+
view.setUint32(0, getCrc32(this._arr));
111+
const checksum = new Uint8Array(checksumArrayBuf);
112+
113+
const array = new Uint8Array([...checksum, ...this._arr]);
114+
115+
const result = encode(array);
116+
const matches = result.match(/.{1,5}/g);
117+
if (!matches) {
118+
// This should only happen if there's no character, which is unreachable.
119+
throw new Error();
120+
}
121+
return matches.join('-');
122+
}
123+
124+
public toString(): string {
125+
return this.toText();
126+
}
127+
128+
/**
129+
* Serializes to JSON
130+
* @returns {JsonnablePrincipal} a JSON object with a single key, {@link JSON_KEY_PRINCIPAL}, whose value is the principal as a string
131+
*/
132+
public toJSON(): JsonnablePrincipal {
133+
return { [JSON_KEY_PRINCIPAL]: this.toText() };
134+
}
135+
136+
/**
137+
* Utility method taking a Principal to compare against. Used for determining canister ranges in certificate verification
138+
* @param {Principal} other - a {@link Principal} to compare
139+
* @returns {'lt' | 'eq' | 'gt'} `'lt' | 'eq' | 'gt'` a string, representing less than, equal to, or greater than
140+
*/
141+
public compareTo(other: Principal): 'lt' | 'eq' | 'gt' {
142+
for (let i = 0; i < Math.min(this._arr.length, other._arr.length); i++) {
143+
if (this._arr[i] < other._arr[i]) return 'lt';
144+
else if (this._arr[i] > other._arr[i]) return 'gt';
145+
}
146+
// Here, at least one principal is a prefix of the other principal (they could be the same)
147+
if (this._arr.length < other._arr.length) return 'lt';
148+
if (this._arr.length > other._arr.length) return 'gt';
149+
return 'eq';
150+
}
151+
152+
/**
153+
* Utility method checking whether a provided Principal is less than or equal to the current one using the {@link Principal.compareTo} method
154+
* @param other a {@link Principal} to compare
155+
* @returns {boolean} boolean
156+
*/
157+
public ltEq(other: Principal): boolean {
158+
const cmp = this.compareTo(other);
159+
return cmp == 'lt' || cmp == 'eq';
160+
}
161+
162+
/**
163+
* Utility method checking whether a provided Principal is greater than or equal to the current one using the {@link Principal.compareTo} method
164+
* @param other a {@link Principal} to compare
165+
* @returns {boolean} boolean
166+
*/
167+
public gtEq(other: Principal): boolean {
168+
const cmp = this.compareTo(other);
169+
return cmp == 'gt' || cmp == 'eq';
170+
}
171+
}

packages/principal/src/utils/base32.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ lookupTable['0'] = lookupTable.o;
1111
lookupTable['1'] = lookupTable.i;
1212

1313
/**
14-
* @param input The input array to encode.
14+
* @param input The Uint8Array to encode.
1515
* @returns A Base32 string encoding the input.
1616
*/
1717
export function encode(input: Uint8Array): string {

packages/principal/src/utils/getCrc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const lookUpTable: Uint32Array = new Uint32Array([
3737

3838
/**
3939
* Calculate the CRC32 of a Uint8Array.
40-
* @param buf The BufferLike to calculate the CRC32 of.
40+
* @param buf The Uint8Array to calculate the CRC32 of.
4141
*/
4242
export function getCrc32(buf: Uint8Array): number {
4343
let crc = -1;

0 commit comments

Comments
 (0)