@bsv/sdk 1.9.18 → 1.9.20
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/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/certificates/MasterCertificate.js +9 -2
- package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
- package/dist/cjs/src/primitives/DRBG.js +11 -6
- package/dist/cjs/src/primitives/DRBG.js.map +1 -1
- package/dist/cjs/src/primitives/ECDSA.js +1 -1
- package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
- package/dist/cjs/src/primitives/Hash.js +6 -5
- package/dist/cjs/src/primitives/Hash.js.map +1 -1
- package/dist/cjs/src/primitives/hex.js +33 -0
- package/dist/cjs/src/primitives/hex.js.map +1 -0
- package/dist/cjs/src/primitives/utils.js +69 -59
- package/dist/cjs/src/primitives/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/certificates/MasterCertificate.js +9 -2
- package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
- package/dist/esm/src/primitives/DRBG.js +11 -6
- package/dist/esm/src/primitives/DRBG.js.map +1 -1
- package/dist/esm/src/primitives/ECDSA.js +1 -1
- package/dist/esm/src/primitives/ECDSA.js.map +1 -1
- package/dist/esm/src/primitives/Hash.js +6 -5
- package/dist/esm/src/primitives/Hash.js.map +1 -1
- package/dist/esm/src/primitives/hex.js +29 -0
- package/dist/esm/src/primitives/hex.js.map +1 -0
- package/dist/esm/src/primitives/utils.js +69 -59
- package/dist/esm/src/primitives/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
- package/dist/types/src/primitives/DRBG.d.ts +2 -1
- package/dist/types/src/primitives/DRBG.d.ts.map +1 -1
- package/dist/types/src/primitives/Hash.d.ts.map +1 -1
- package/dist/types/src/primitives/hex.d.ts +3 -0
- package/dist/types/src/primitives/hex.d.ts.map +1 -0
- package/dist/types/src/primitives/utils.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/primitives.md +2 -2
- package/package.json +1 -1
- package/src/auth/__tests/Peer.test.ts +2 -1
- package/src/auth/certificates/MasterCertificate.ts +9 -2
- package/src/auth/certificates/__tests/MasterCertificate.test.ts +46 -9
- package/src/primitives/DRBG.ts +14 -7
- package/src/primitives/ECDSA.ts +1 -1
- package/src/primitives/Hash.ts +9 -6
- package/src/primitives/__tests/DRBG.test.ts +272 -9
- package/src/primitives/__tests/ECDSA.test.ts +29 -0
- package/src/primitives/__tests/HMAC.test.ts +13 -2
- package/src/primitives/__tests/Hash.test.ts +24 -0
- package/src/primitives/__tests/hex.test.ts +57 -0
- package/src/primitives/__tests/utils.test.ts +39 -0
- package/src/primitives/hex.ts +35 -0
- package/src/primitives/utils.ts +71 -65
- package/src/script/__tests/Script.test.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
|
|
3
|
+
import { assertValidHex, normalizeHex } from '../../primitives/hex'
|
|
4
|
+
|
|
5
|
+
describe('hex utils', () => {
|
|
6
|
+
describe('assertValidHex', () => {
|
|
7
|
+
it('should not throw on valid hex strings', () => {
|
|
8
|
+
expect(() => assertValidHex('')).not.toThrow() // empty is allowed
|
|
9
|
+
expect(() => assertValidHex('00')).not.toThrow()
|
|
10
|
+
expect(() => assertValidHex('abcdef')).not.toThrow()
|
|
11
|
+
expect(() => assertValidHex('ABCDEF')).not.toThrow()
|
|
12
|
+
expect(() => assertValidHex('1234567890')).not.toThrow()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should throw on non-hex characters', () => {
|
|
16
|
+
expect(() => assertValidHex('zz')).toThrow('Invalid hex string')
|
|
17
|
+
expect(() => assertValidHex('0x1234')).toThrow('Invalid hex string')
|
|
18
|
+
expect(() => assertValidHex('12 34')).toThrow('Invalid hex string')
|
|
19
|
+
expect(() => assertValidHex('g1')).toThrow('Invalid hex string')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// ❌ old behavior: empty string was considered invalid
|
|
23
|
+
// it('should throw on empty string', () => {
|
|
24
|
+
// expect(() => assertValidHex('')).toThrow('Invalid hex string')
|
|
25
|
+
// })
|
|
26
|
+
|
|
27
|
+
it('should throw on undefined or null', () => {
|
|
28
|
+
expect(() => assertValidHex(undefined as any)).toThrow('Invalid hex string')
|
|
29
|
+
expect(() => assertValidHex(null as any)).toThrow('Invalid hex string')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('normalizeHex', () => {
|
|
34
|
+
it('should return lowercase hex', () => {
|
|
35
|
+
expect(normalizeHex('ABCD')).toBe('abcd')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should prepend 0 to odd-length hex strings', () => {
|
|
39
|
+
expect(normalizeHex('abc')).toBe('0abc')
|
|
40
|
+
expect(normalizeHex('f')).toBe('0f')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should leave even-length hex strings untouched (except lowercase)', () => {
|
|
44
|
+
expect(normalizeHex('AABB')).toBe('aabb')
|
|
45
|
+
expect(normalizeHex('001122')).toBe('001122')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return empty string unchanged', () => {
|
|
49
|
+
expect(normalizeHex('')).toBe('')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should throw on invalid hex', () => {
|
|
53
|
+
expect(() => normalizeHex('xyz')).toThrow('Invalid hex string')
|
|
54
|
+
expect(() => normalizeHex('12 34')).toThrow('Invalid hex string')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -320,3 +320,42 @@ describe('verifyNotNull', () => {
|
|
|
320
320
|
expect(() => verifyNotNull(undefined, 'Another custom error')).toThrow('Another custom error')
|
|
321
321
|
})
|
|
322
322
|
})
|
|
323
|
+
|
|
324
|
+
describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
|
|
325
|
+
|
|
326
|
+
it('replaces invalid 2-byte sequences with U+FFFD', () => {
|
|
327
|
+
// 0xC2 should expect a continuation byte 0x80–0xBF
|
|
328
|
+
const arr = [0xC2, 0x20] // 0x20 is INVALID continuation
|
|
329
|
+
const str = toUTF8(arr)
|
|
330
|
+
expect(str).toBe('\uFFFD')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('decodes valid 3-byte sequences', () => {
|
|
334
|
+
const euro = [0xE2, 0x82, 0xAC]
|
|
335
|
+
expect(toUTF8(euro)).toBe('€')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('replaces invalid 3-byte sequences', () => {
|
|
339
|
+
// Middle byte invalid
|
|
340
|
+
const arr = [0xE2, 0x20, 0xAC]
|
|
341
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('decodes valid 4-byte sequences into surrogate pairs', () => {
|
|
345
|
+
const smile = [0xF0, 0x9F, 0x98, 0x80] // 😀
|
|
346
|
+
expect(toUTF8(smile)).toBe('😀')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('replaces invalid 4-byte sequences with U+FFFD', () => {
|
|
350
|
+
// 0x9F is valid, 0x20 is INVALID continuation for byte 3
|
|
351
|
+
const arr = [0xF0, 0x9F, 0x20, 0x80]
|
|
352
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('replaces incomplete UTF-8 sequence at end', () => {
|
|
356
|
+
const arr = [0xE2] // incomplete 3-byte seq
|
|
357
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
})
|
|
361
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/primitives/hex.ts
|
|
2
|
+
|
|
3
|
+
// Accepts empty string because empty byte arrays are valid in Bitcoin.
|
|
4
|
+
const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/
|
|
5
|
+
|
|
6
|
+
export function assertValidHex (msg: string): void {
|
|
7
|
+
if (typeof msg !== 'string') {
|
|
8
|
+
console.error('assertValidHex FAIL (non-string):', msg)
|
|
9
|
+
throw new Error('Invalid hex string')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// allow empty
|
|
13
|
+
if (msg.length === 0) return
|
|
14
|
+
|
|
15
|
+
if (!PURE_HEX_REGEX.test(msg)) {
|
|
16
|
+
console.error('assertValidHex FAIL (bad hex):', msg)
|
|
17
|
+
throw new Error('Invalid hex string')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeHex (msg: string): string {
|
|
22
|
+
assertValidHex(msg)
|
|
23
|
+
|
|
24
|
+
// If empty, return empty — never force to "00"
|
|
25
|
+
if (msg.length === 0) return ''
|
|
26
|
+
|
|
27
|
+
let normalized = msg.toLowerCase()
|
|
28
|
+
|
|
29
|
+
// Pad odd-length hex
|
|
30
|
+
if (normalized.length % 2 !== 0) {
|
|
31
|
+
normalized = '0' + normalized
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalized
|
|
35
|
+
}
|
package/src/primitives/utils.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import BigNumber from './BigNumber.js'
|
|
2
2
|
import { hash256 } from './Hash.js'
|
|
3
|
+
import { assertValidHex } from './hex.js'
|
|
3
4
|
|
|
4
5
|
const BufferCtor =
|
|
5
6
|
typeof globalThis !== 'undefined' ? (globalThis as any).Buffer : undefined
|
|
6
7
|
const CAN_USE_BUFFER =
|
|
7
8
|
BufferCtor != null && typeof BufferCtor.from === 'function'
|
|
8
|
-
const PURE_HEX_REGEX = /^[0-9a-fA-F]+$/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Prepends a '0' to an odd character length word to ensure it has an even number of characters.
|
|
@@ -80,28 +80,19 @@ for (let i = 0; i < 6; i++) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const hexToArray = (msg: string): number[] => {
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
assertValidHex(msg)
|
|
84
|
+
const normalized = msg.length % 2 === 0 ? msg : '0' + msg
|
|
85
|
+
if (CAN_USE_BUFFER) {
|
|
85
86
|
return Array.from(BufferCtor.from(normalized, 'hex'))
|
|
86
87
|
}
|
|
87
|
-
const
|
|
88
|
-
let
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
if (nibble === -1) {
|
|
94
|
-
nibble = value
|
|
95
|
-
} else {
|
|
96
|
-
res[size++] = (nibble << 4) | value
|
|
97
|
-
nibble = -1
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (nibble !== -1) {
|
|
101
|
-
res[size++] = nibble
|
|
88
|
+
const out = new Array(normalized.length / 2)
|
|
89
|
+
let o = 0
|
|
90
|
+
for (let i = 0; i < normalized.length; i += 2) {
|
|
91
|
+
const hi = HEX_CHAR_TO_VALUE[normalized.charCodeAt(i)]
|
|
92
|
+
const lo = HEX_CHAR_TO_VALUE[normalized.charCodeAt(i + 1)]
|
|
93
|
+
out[o++] = (hi << 4) | lo
|
|
102
94
|
}
|
|
103
|
-
|
|
104
|
-
return res
|
|
95
|
+
return out
|
|
105
96
|
}
|
|
106
97
|
|
|
107
98
|
export function base64ToArray (msg: string): number[] {
|
|
@@ -233,69 +224,84 @@ function utf8ToArray (str: string): number[] {
|
|
|
233
224
|
*/
|
|
234
225
|
export const toUTF8 = (arr: number[]): string => {
|
|
235
226
|
let result = ''
|
|
236
|
-
|
|
237
|
-
|
|
227
|
+
const replacementChar = '\uFFFD'
|
|
238
228
|
for (let i = 0; i < arr.length; i++) {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (skip > 0) {
|
|
243
|
-
skip--
|
|
229
|
+
const byte1 = arr[i]
|
|
230
|
+
if (byte1 <= 0x7f) {
|
|
231
|
+
result += String.fromCharCode(byte1)
|
|
244
232
|
continue
|
|
245
233
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (byte <= 0x7f) {
|
|
249
|
-
result += String.fromCharCode(byte)
|
|
250
|
-
continue
|
|
234
|
+
const emitReplacement = (): void => {
|
|
235
|
+
result += replacementChar
|
|
251
236
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
237
|
+
if (byte1 >= 0xc0 && byte1 <= 0xdf) {
|
|
238
|
+
if (i + 1 >= arr.length) {
|
|
239
|
+
emitReplacement()
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
const byte2 = arr[i + 1]
|
|
243
|
+
if ((byte2 & 0xc0) !== 0x80) {
|
|
244
|
+
emitReplacement()
|
|
245
|
+
i += 1
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
const codePoint = ((byte1 & 0x1f) << 6) | (byte2 & 0x3f)
|
|
260
249
|
result += String.fromCharCode(codePoint)
|
|
250
|
+
i += 1
|
|
261
251
|
continue
|
|
262
252
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
253
|
+
if (byte1 >= 0xe0 && byte1 <= 0xef) {
|
|
254
|
+
if (i + 2 >= arr.length) {
|
|
255
|
+
emitReplacement()
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
const byte2 = arr[i + 1]
|
|
259
|
+
const byte3 = arr[i + 2]
|
|
260
|
+
if ((byte2 & 0xc0) !== 0x80 || (byte3 & 0xc0) !== 0x80) {
|
|
261
|
+
emitReplacement()
|
|
262
|
+
i += 2
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
271
265
|
const codePoint =
|
|
272
|
-
((
|
|
266
|
+
((byte1 & 0x0f) << 12) |
|
|
267
|
+
((byte2 & 0x3f) << 6) |
|
|
268
|
+
(byte3 & 0x3f)
|
|
269
|
+
|
|
273
270
|
result += String.fromCharCode(codePoint)
|
|
271
|
+
i += 2
|
|
274
272
|
continue
|
|
275
273
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
274
|
+
if (byte1 >= 0xf0 && byte1 <= 0xf7) {
|
|
275
|
+
if (i + 3 >= arr.length) {
|
|
276
|
+
emitReplacement()
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
const byte2 = arr[i + 1]
|
|
280
|
+
const byte3 = arr[i + 2]
|
|
281
|
+
const byte4 = arr[i + 3]
|
|
282
|
+
if (
|
|
283
|
+
(byte2 & 0xc0) !== 0x80 ||
|
|
284
|
+
(byte3 & 0xc0) !== 0x80 ||
|
|
285
|
+
(byte4 & 0xc0) !== 0x80
|
|
286
|
+
) {
|
|
287
|
+
emitReplacement()
|
|
288
|
+
i += 3
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
285
291
|
const codePoint =
|
|
286
|
-
((
|
|
292
|
+
((byte1 & 0x07) << 18) |
|
|
287
293
|
((byte2 & 0x3f) << 12) |
|
|
288
294
|
((byte3 & 0x3f) << 6) |
|
|
289
295
|
(byte4 & 0x3f)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
const offset = codePoint - 0x10000
|
|
297
|
+
const highSurrogate = 0xd800 + (offset >> 10)
|
|
298
|
+
const lowSurrogate = 0xdc00 + (offset & 0x3ff)
|
|
299
|
+
result += String.fromCharCode(highSurrogate, lowSurrogate)
|
|
300
|
+
i += 3
|
|
295
301
|
continue
|
|
296
302
|
}
|
|
303
|
+
emitReplacement()
|
|
297
304
|
}
|
|
298
|
-
|
|
299
305
|
return result
|
|
300
306
|
}
|
|
301
307
|
|
|
@@ -286,7 +286,7 @@ describe('Script', () => {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// Expect the function to throw an error with the specified message
|
|
289
|
-
expect(createScript).toThrow('
|
|
289
|
+
expect(createScript).toThrow('Invalid hex string')
|
|
290
290
|
})
|
|
291
291
|
|
|
292
292
|
it('should parse this long PUSHDATA1 script in ASM', () => {
|