@wallaby-cash/cryptography 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @wallaby-cash/cryptography
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Initial release of @wallaby-cash/cryptography package
8
+
9
+ This release includes:
10
+ - Identity creation with public/private key pairs
11
+ - Message signing and verification
12
+ - Public key encryption and decryption
13
+ - Keccak256 hashing utilities
14
+ - Public key recovery from signatures
15
+ - Public key derivation from private keys
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @wallaby-cash/cryptography
2
+
3
+ Cryptography utilities for Wallaby Cash, providing Ethereum-compatible cryptographic operations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @wallaby-cash/cryptography
9
+ # or
10
+ bun add @wallaby-cash/cryptography
11
+ # or
12
+ yarn add @wallaby-cash/cryptography
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - **Identity Creation**: Generate new cryptographic identities with public/private key pairs
18
+ - **Signing**: Sign messages with private keys
19
+ - **Encryption**: Encrypt and decrypt data using public/private key cryptography
20
+ - **Hashing**: Keccak256 hashing utilities
21
+ - **Key Recovery**: Recover public keys from signatures
22
+ - **Key Derivation**: Derive public keys from private keys
23
+
24
+ ## Usage
25
+
26
+ ```typescript
27
+ import {
28
+ createIdentity,
29
+ sign,
30
+ encryptWithPublicKey,
31
+ decryptWithPrivateKey,
32
+ hash,
33
+ publicKeyByPrivateKey,
34
+ recoverPublicKey
35
+ } from '@wallaby-cash/cryptography';
36
+
37
+ // Create a new identity
38
+ const identity = createIdentity();
39
+ console.log('Public Key:', identity.publicKey);
40
+ console.log('Private Key:', identity.privateKey);
41
+
42
+ // Sign a message
43
+ const message = 'Hello, Wallaby!';
44
+ const signature = sign(identity.privateKey, message);
45
+
46
+ // Hash data
47
+ const hashed = hash.keccak256('some data');
48
+
49
+ // Encrypt and decrypt
50
+ const encrypted = await encryptWithPublicKey(identity.publicKey, 'secret message');
51
+ const decrypted = await decryptWithPrivateKey(identity.privateKey, encrypted);
52
+
53
+ // Derive public key from private key
54
+ const pubKey = publicKeyByPrivateKey(identity.privateKey);
55
+
56
+ // Recover public key from signature
57
+ const recoveredPubKey = recoverPublicKey(signature, message);
58
+ ```
59
+
60
+ ## API Reference
61
+
62
+ ### `createIdentity()`
63
+ Generates a new cryptographic identity with a public/private key pair.
64
+
65
+ **Returns:** `{ publicKey: string, privateKey: string }`
66
+
67
+ ### `sign(privateKey: string, message: string)`
68
+ Signs a message with a private key.
69
+
70
+ **Parameters:**
71
+ - `privateKey`: The private key to sign with
72
+ - `message`: The message to sign
73
+
74
+ **Returns:** Signature object
75
+
76
+ ### `encryptWithPublicKey(publicKey: string, data: string)`
77
+ Encrypts data using a public key.
78
+
79
+ **Parameters:**
80
+ - `publicKey`: The recipient's public key
81
+ - `data`: The data to encrypt
82
+
83
+ **Returns:** `Promise<EncryptedData>`
84
+
85
+ ### `decryptWithPrivateKey(privateKey: string, encrypted: EncryptedData)`
86
+ Decrypts data using a private key.
87
+
88
+ **Parameters:**
89
+ - `privateKey`: The private key to decrypt with
90
+ - `encrypted`: The encrypted data object
91
+
92
+ **Returns:** `Promise<string>`
93
+
94
+ ### `hash.keccak256(data: string)`
95
+ Hashes data using Keccak256.
96
+
97
+ **Parameters:**
98
+ - `data`: The data to hash
99
+
100
+ **Returns:** Hash string
101
+
102
+ ### `publicKeyByPrivateKey(privateKey: string)`
103
+ Derives a public key from a private key.
104
+
105
+ **Parameters:**
106
+ - `privateKey`: The private key
107
+
108
+ **Returns:** `string` - The derived public key
109
+
110
+ ### `recoverPublicKey(signature: Signature, message: string)`
111
+ Recovers the public key from a signature and message.
112
+
113
+ **Parameters:**
114
+ - `signature`: The signature object
115
+ - `message`: The original message
116
+
117
+ **Returns:** `string` - The recovered public key
118
+
119
+ ## License
120
+
121
+ MIT
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@wallaby-cash/cryptography",
3
+ "version": "1.0.0",
4
+ "description": "Cryptography utilities for Wallaby Cash",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./*": "./src/*.ts"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "keywords": [
15
+ "cryptography",
16
+ "encryption",
17
+ "ethereum",
18
+ "secp256k1",
19
+ "wallaby"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/wallaby-cash/sdk-monorepo.git",
25
+ "directory": "packages/cryptography"
26
+ },
27
+ "scripts": {
28
+ "lint": "eslint . --max-warnings 0",
29
+ "generate:component": "turbo gen react-component",
30
+ "check-types": "tsc --noEmit"
31
+ },
32
+ "devDependencies": {
33
+ "@wallaby-cash/eslint-config": "*",
34
+ "@wallaby-cash/typescript-config": "*",
35
+ "@types/node": "^22.15.3",
36
+ "@types/react": "19.2.2",
37
+ "@types/react-dom": "19.2.2",
38
+ "eslint": "^9.39.1",
39
+ "typescript": "5.9.2"
40
+ },
41
+ "dependencies": {
42
+ "ethereum-cryptography": "^3.2.0",
43
+ "react": "^19.2.0",
44
+ "react-dom": "^19.2.0"
45
+ }
46
+ }
@@ -0,0 +1,66 @@
1
+ import { getRandomBytesSync as randomBytes } from 'ethereum-cryptography/random.js';
2
+ import { addLeading0x, stripHexPrefix, concatUint8Arrays } from './util';
3
+ import { publicKeyByPrivateKey } from './publicKeyByPrivateKey';
4
+ import { bytesToHex } from 'ethereum-cryptography/utils';
5
+ import { keccak256 } from 'ethereum-cryptography/keccak';
6
+
7
+ export const DEFAULT_ENTROPY_BYTES = 32;
8
+ export const MINIMUM_SHANNON_ENTROPY = 4;
9
+
10
+ /**
11
+ * creates a new private key
12
+ * @param { Uint8Array } entropy - optional entropy to create the private key
13
+ * @returns a new private key
14
+ */
15
+ export const createPrivateKey = (entropy?: Uint8Array) => {
16
+ if (entropy) {
17
+ if (!(entropy instanceof Uint8Array) || entropy.length < DEFAULT_ENTROPY_BYTES) {
18
+ throw new Error(`entropy must be a Uint8Array of at least ${DEFAULT_ENTROPY_BYTES} bytes`);
19
+ }
20
+
21
+ // Check byte diversity
22
+ const uniqueBytes = new Set(entropy);
23
+ if (uniqueBytes.size < Math.min(8, entropy.length / 4)) {
24
+ throw new Error(`entropy is too repetitive (only ${uniqueBytes.size} unique byte values)`);
25
+ }
26
+
27
+ // Estimate Shannon entropy
28
+ const byteCounts = new Uint32Array(256);
29
+ entropy.forEach((b) => byteCounts[b]!++);
30
+ const total = entropy.length;
31
+ let shannonEntropy = 0;
32
+ for (let i = 0; i < 256; i++) {
33
+ if (byteCounts[i]! > 0) {
34
+ const p = byteCounts[i]! / total;
35
+ shannonEntropy -= p * Math.log2(p);
36
+ }
37
+ }
38
+ if (shannonEntropy < MINIMUM_SHANNON_ENTROPY) {
39
+ throw new Error(`entropy has low Shannon entropy (${shannonEntropy.toFixed(2)} bits/byte)`);
40
+ }
41
+
42
+ const outerHex = keccak256(entropy);
43
+ return addLeading0x(bytesToHex(outerHex));
44
+ } else {
45
+ const innerHex = keccak256(concatUint8Arrays([randomBytes(32), randomBytes(32)]));
46
+ const middleHex = concatUint8Arrays([concatUint8Arrays([randomBytes(32), innerHex]), randomBytes(32)]);
47
+ const outerHex = keccak256(middleHex);
48
+ return addLeading0x(bytesToHex(outerHex));
49
+ }
50
+ };
51
+
52
+ /**
53
+ * creates a new identity
54
+ * @param { Uint8Array } entropy - optional entropy to create the private key
55
+ * @returns a new pair of private and public key
56
+ */
57
+ export const createIdentity = (entropy?: Uint8Array) => {
58
+ const privateKey = createPrivateKey(entropy);
59
+
60
+ const walletPublicKey = publicKeyByPrivateKey(privateKey);
61
+ const identity = {
62
+ privateKey: privateKey,
63
+ publicKey: stripHexPrefix(walletPublicKey),
64
+ };
65
+ return identity;
66
+ };
@@ -0,0 +1,10 @@
1
+ import { stripHexPrefix } from './util';
2
+ import { decrypt } from './encryption-utils';
3
+ import { Encrypted } from './types';
4
+
5
+ export const decryptWithPrivateKey = (privateKey: string, encrypted: Encrypted) => {
6
+ // remove '0x' from privateKey
7
+ const twoStripped = stripHexPrefix(privateKey);
8
+
9
+ return decrypt(twoStripped, encrypted);
10
+ };
@@ -0,0 +1,12 @@
1
+ import { decompress } from './util';
2
+ import { encrypt } from './encryption-utils';
3
+ import { EncryptionOptions } from './types';
4
+
5
+ export const encryptWithPublicKey = (publicKey: string, message: string, options?: EncryptionOptions) => {
6
+ // ensure its an uncompressed publicKey
7
+ const decompressedKey = decompress(publicKey);
8
+
9
+ // re-add the compression-flag
10
+ const pubString = '04' + decompressedKey;
11
+ return encrypt(pubString, message, options);
12
+ };
@@ -0,0 +1,93 @@
1
+ import { sha512 } from 'ethereum-cryptography/sha512.js';
2
+ import { secp256k1 } from 'ethereum-cryptography/secp256k1';
3
+ import { getRandomBytesSync as randomBytes } from 'ethereum-cryptography/random.js';
4
+ import { hexToBytes, bytesToHex, bytesToUtf8 } from 'ethereum-cryptography/utils.js';
5
+ import { encrypt as aesEncrypt, decrypt as aesDecrypt } from 'ethereum-cryptography/aes.js';
6
+ import { Encrypted, EncryptionOptions } from './types';
7
+ import { hmacSha256Sign } from './sign';
8
+ import { concatUint8Arrays, utf8ToBytes } from './util';
9
+
10
+ /**
11
+ * See https://github.com/bitchan/eccrypto for the original implementation that eth-crypto used, it's ancient and not maintained.
12
+ */
13
+
14
+ /**
15
+ * Encrypts a message using the recipient's public key.
16
+ * @param {string} publicKeyTo - The recipient's public key.
17
+ * @param {string} msg - The message to encrypt.
18
+ * @returns {Encrypted} The encrypted message.
19
+ */
20
+
21
+ export const encrypt = (publicKeyTo: string, msg: string, options?: EncryptionOptions): Encrypted => {
22
+ const ephemPrivateKey = options?.ephemPrivateKey ? hexToBytes(options.ephemPrivateKey) : randomBytes(32);
23
+ const iv = randomBytes(16);
24
+
25
+ const compressedPub = secp256k1.getPublicKey(ephemPrivateKey); // defaults to compressed format
26
+ const point = secp256k1.ProjectivePoint.fromHex(compressedPub); // decompress the pub into an EC point
27
+ const ephemPublicKey = point.toRawBytes(false); // Get uncompressed SEC1 format pub
28
+ const sharedSecret = secp256k1.getSharedSecret(ephemPrivateKey, hexToBytes(publicKeyTo), true).slice(1);
29
+ const hash = sha512(sharedSecret);
30
+ const encryptionKey = hash.subarray(0, 32);
31
+ const macKey = hash.subarray(32);
32
+ const message = utf8ToBytes(msg);
33
+ const data = aesEncrypt(message, encryptionKey, iv, 'aes-256-cbc');
34
+ const dataToMac = concatUint8Arrays([iv, ephemPublicKey, data]);
35
+ const mac = hmacSha256Sign(macKey, dataToMac);
36
+
37
+ return {
38
+ iv: bytesToHex(iv),
39
+ ephemPublicKey: bytesToHex(ephemPublicKey),
40
+ ciphertext: bytesToHex(data),
41
+ mac: bytesToHex(mac),
42
+ };
43
+ };
44
+
45
+ /**
46
+ * Decrypts an encrypted message using the recipient's private key.
47
+ * @param {string} privateKey - The recipient's private key.
48
+ * @param {Encrypted} opts - The encrypted message.
49
+ * @returns {string} The decrypted message.
50
+ */
51
+ export const decrypt = (privateKey: string, opts: Encrypted) => {
52
+ let sharedSecret: Uint8Array;
53
+ try {
54
+ sharedSecret = secp256k1.getSharedSecret(hexToBytes(privateKey), opts.ephemPublicKey, true).slice(1);
55
+ } catch (e) {
56
+ throw new Error(`Invalid MAC: data integrity check failed: ${e}`);
57
+ }
58
+
59
+ const hash = sha512(sharedSecret);
60
+ const encryptionKey = hash.subarray(0, 32);
61
+ const macKey = hash.subarray(32);
62
+
63
+ const ciphertext = hexToBytes(opts.ciphertext);
64
+ const iv = hexToBytes(opts.iv);
65
+ const ephemPublicKey = hexToBytes(opts.ephemPublicKey);
66
+ const receivedMac = hexToBytes(opts.mac);
67
+
68
+ // Recompute MAC
69
+ const dataToMac = concatUint8Arrays([iv, ephemPublicKey, ciphertext]);
70
+ const expectedMac = hmacSha256Sign(macKey, dataToMac);
71
+
72
+ if (!constantTimeEqual(expectedMac, receivedMac)) {
73
+ throw new Error('Invalid MAC: data integrity check failed');
74
+ }
75
+
76
+ const decrypted = aesDecrypt(ciphertext, encryptionKey, iv, 'aes-256-cbc');
77
+ return bytesToUtf8(decrypted);
78
+ };
79
+
80
+ /**
81
+ * Compares two Uint8Arrays in constant time to prevent timing attacks.
82
+ * @param {Uint8Array} a - The first array.
83
+ * @param {Uint8Array} b - The second array.
84
+ * @returns {boolean} True if the arrays are equal, false otherwise.
85
+ */
86
+ function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
87
+ if (a.length !== b.length) return false;
88
+ let result = 0;
89
+ for (let i = 0; i < a.length; i++) {
90
+ result |= a[i]! ^ b[i]!;
91
+ }
92
+ return result === 0;
93
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils.js';
2
+ import { keccak256 as _keccak256 } from 'ethereum-cryptography/keccak.js';
3
+ import { addLeading0x, utf8ToBytes } from './util';
4
+
5
+ const solidityPackedKeccak256 = (value: string) => {
6
+ const bytes = utf8ToBytes(value);
7
+ const hex = addLeading0x(bytesToHex(bytes));
8
+ const hash = _keccak256(hexToBytes(hex));
9
+ return addLeading0x(bytesToHex(hash));
10
+ };
11
+
12
+ export const keccak256 = (params: string) => {
13
+ return solidityPackedKeccak256(params);
14
+ };
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { createIdentity } from './createIdentity';
2
+ import { sign } from './sign';
3
+ import { encryptWithPublicKey } from './encryptWithPublicKey';
4
+ import { decryptWithPrivateKey } from './decryptWithPrivateKey';
5
+ import { keccak256 } from './hash';
6
+ import { publicKeyByPrivateKey } from './publicKeyByPrivateKey';
7
+ import { recoverPublicKey } from './recoverPublicKey';
8
+
9
+ const hash = {
10
+ keccak256,
11
+ };
12
+
13
+ export {
14
+ createIdentity,
15
+ decryptWithPrivateKey,
16
+ encryptWithPublicKey,
17
+ hash,
18
+ keccak256,
19
+ publicKeyByPrivateKey,
20
+ recoverPublicKey,
21
+ sign,
22
+ };
23
+
24
+ export default {
25
+ createIdentity,
26
+ decryptWithPrivateKey,
27
+ encryptWithPublicKey,
28
+ hash,
29
+ keccak256,
30
+ publicKeyByPrivateKey,
31
+ recoverPublicKey,
32
+ sign,
33
+ };
@@ -0,0 +1,17 @@
1
+ import { decompress, stripHexPrefix } from './util';
2
+ import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
3
+ import { bytesToHex } from 'ethereum-cryptography/utils';
4
+
5
+ /**
6
+ * Generate publicKey from the privateKey.
7
+ * This creates the uncompressed publicKey,
8
+ * where 04 has stripped from left
9
+ * @returns {string}
10
+ */
11
+
12
+ export const publicKeyByPrivateKey = (privateKey: string) => {
13
+ const key = stripHexPrefix(privateKey);
14
+ const compressedPub = secp256k1.getPublicKey(key); // defaults to compressed format
15
+ const point = secp256k1.ProjectivePoint.fromHex(compressedPub); // decompress the pub into an EC point
16
+ return decompress(bytesToHex(point.toRawBytes(false))); // Get uncompressed SEC1 format pub
17
+ };
@@ -0,0 +1,26 @@
1
+ import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat';
2
+ import { stripHexPrefix } from './util';
3
+ import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils';
4
+
5
+ /**
6
+ * returns the publicKey for the privateKey with which the messageHash was signed
7
+ * @param {string} signature
8
+ * @param {string} hash
9
+ * @return {string} publicKey
10
+ */
11
+ export const recoverPublicKey = (signature: string, hash: string) => {
12
+ const noHex = stripHexPrefix(signature);
13
+
14
+ // split into v-value and sig
15
+ const sigOnly = noHex.substring(0, noHex.length - 2); // all but last 2 chars
16
+ const vValue = noHex.slice(-2); // last 2 chars
17
+
18
+ const recoveryNumber = vValue === '1c' ? 1 : 0;
19
+
20
+ let pubKey = bytesToHex(ecdsaRecover(hexToBytes(sigOnly), recoveryNumber, hexToBytes(stripHexPrefix(hash)), false));
21
+
22
+ // remove trailing '04'
23
+ pubKey = pubKey.slice(2);
24
+
25
+ return pubKey;
26
+ };
package/src/sign.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
2
+ import { hmac } from '@noble/hashes/hmac';
3
+ import { sha256 } from '@noble/hashes/sha2';
4
+ import { hexToBytes } from 'ethereum-cryptography/utils';
5
+ import { addLeading0x, isHexString, stripHexPrefix } from './util';
6
+
7
+ /**
8
+ * signs the given message
9
+ * @param {string} privateKey
10
+ * @param {string} hash
11
+ * @return {string} hexString
12
+ */
13
+ export const sign = (privateKey: string, hash: string) => {
14
+ const hashWith0x = addLeading0x(hash);
15
+ if (hashWith0x.length !== 66 || !isHexString(hashWith0x)) throw new Error('Can only sign hashes, given: ' + hash);
16
+
17
+ const sigObj = secp256k1.sign(hexToBytes(stripHexPrefix(hash)), hexToBytes(stripHexPrefix(privateKey)));
18
+
19
+ const recoveryId = sigObj.recovery === 1 ? '1c' : '1b';
20
+ const newSignature = '0x' + sigObj.toCompactHex() + recoveryId;
21
+ return newSignature;
22
+ };
23
+
24
+ export const hmacSha256Sign = (key: Uint8Array, msg: Uint8Array) => {
25
+ const result = hmac(sha256, key, msg);
26
+ return result;
27
+ };
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type Encrypted = {
2
+ iv: string;
3
+ ephemPublicKey: string;
4
+ ciphertext: string;
5
+ mac: string;
6
+ };
7
+
8
+ export type EncryptionOptions = {
9
+ ephemPrivateKey?: string;
10
+ };
package/src/util.ts ADDED
@@ -0,0 +1,110 @@
1
+ import { hexToBytes } from 'ethereum-cryptography/utils';
2
+
3
+ /**
4
+ * Returns a `Boolean` on whether or not the a `String` starts with '0x'
5
+ * @param str the string input value
6
+ * @return a boolean if it is or is not hex prefixed
7
+ * @throws if the str input is not a string
8
+ */
9
+ export const isHexPrefixed = (str: string) => {
10
+ if (typeof str !== 'string') {
11
+ throw new Error(`[isHexPrefixed] input must be type 'string', received type ${typeof str}`);
12
+ }
13
+
14
+ return str[0] === '0' && str[1] === 'x';
15
+ };
16
+
17
+ /**
18
+ * Removes '0x' from a given `String` if present
19
+ * @param str the string value
20
+ * @returns the string without 0x prefix
21
+ */
22
+ export const stripHexPrefix = (str: string): string => {
23
+ if (typeof str !== 'string') throw new Error(`[stripHexPrefix] input must be type 'string', received ${typeof str}`);
24
+
25
+ return isHexPrefixed(str) ? str.slice(2) : str;
26
+ };
27
+
28
+ /**
29
+ * Is the string a hex string.
30
+ *
31
+ * @param value
32
+ * @param length
33
+ * @returns output the string is a hex string
34
+ */
35
+ export const isHexString = (value: string, length?: number): boolean => {
36
+ if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]+$/)) return false;
37
+
38
+ if (length && value.length !== 2 + 2 * length) return false;
39
+
40
+ return true;
41
+ };
42
+
43
+ /**
44
+ * Adds '0x' to a given `String` if not present
45
+ * @param str the string input value
46
+ * @return the string with a 0x prefix
47
+ */
48
+
49
+ export const addLeading0x = (str: string) => {
50
+ if (!str.startsWith('0x')) return '0x' + str;
51
+ else return str;
52
+ };
53
+
54
+ export const decompress = (startsWith02Or03: string) => {
55
+ const testByteArray = hexToBytes(startsWith02Or03);
56
+ let startsWith04 = startsWith02Or03;
57
+ if (testByteArray.length === 64) {
58
+ startsWith04 = '04' + startsWith02Or03;
59
+ }
60
+ return startsWith04.substring(2);
61
+ };
62
+
63
+ /** Helper function to concat UInt8Arrays mimicking the behaviour of the
64
+ * Buffer.concat function in Node.js
65
+ */
66
+
67
+ export const concatUint8Arrays = (uint8arrays: Uint8Array[]) => {
68
+ const totalLength = uint8arrays.reduce((total, uint8array) => total + uint8array.byteLength, 0);
69
+ const result = new Uint8Array(totalLength);
70
+ let offset = 0;
71
+ uint8arrays.forEach((uint8array) => {
72
+ result.set(uint8array, offset);
73
+ offset += uint8array.byteLength;
74
+ });
75
+ return result;
76
+ };
77
+
78
+ /**
79
+ * Converts a UTF-8 string to a Uint8Array without using TextEncoder, which is not available in mobile
80
+ * @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
81
+ */
82
+ export const utf8ToBytes = (str: string): Uint8Array => {
83
+ if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
84
+ const bytes = [];
85
+
86
+ for (let i = 0; i < str.length; i++) {
87
+ const codePoint = str.codePointAt(i);
88
+
89
+ if (!codePoint) {
90
+ throw new Error('Invalid code point');
91
+ }
92
+
93
+ if (codePoint < 0x80) {
94
+ bytes.push(codePoint);
95
+ } else if (codePoint < 0x800) {
96
+ bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
97
+ } else if (codePoint < 0x10000) {
98
+ bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
99
+ } else {
100
+ i++; // skip one iteration since we have a surrogate pair
101
+ bytes.push(
102
+ 0xf0 | (codePoint >> 18),
103
+ 0x80 | ((codePoint >> 12) & 0x3f),
104
+ 0x80 | ((codePoint >> 6) & 0x3f),
105
+ 0x80 | (codePoint & 0x3f),
106
+ );
107
+ }
108
+ }
109
+ return new Uint8Array(bytes);
110
+ };