@buildonspark/spark-sdk 0.2.11 → 0.2.13

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bare/index.cjs +761 -243
  3. package/dist/bare/index.d.cts +70 -11
  4. package/dist/bare/index.d.ts +70 -11
  5. package/dist/bare/index.js +684 -170
  6. package/dist/{chunk-A5M55UR3.js → chunk-5VWGOHED.js} +499 -8
  7. package/dist/{chunk-3WBPICWC.js → chunk-CKHJFQUA.js} +1 -1
  8. package/dist/{chunk-QNYJGFPD.js → chunk-LX45BCZW.js} +207 -160
  9. package/dist/{chunk-76SYPHOC.js → chunk-TB7DG5CU.js} +2 -2
  10. package/dist/{chunk-6CMNEDBK.js → chunk-XXTWWW6L.js} +1 -1
  11. package/dist/{client-Dd3QnxQu.d.ts → client-D7KDa4Ih.d.ts} +1 -1
  12. package/dist/{client-B9CAWKWz.d.cts → client-DVuA5-7M.d.cts} +1 -1
  13. package/dist/debug.cjs +761 -243
  14. package/dist/debug.d.cts +4 -4
  15. package/dist/debug.d.ts +4 -4
  16. package/dist/debug.js +4 -4
  17. package/dist/graphql/objects/index.d.cts +3 -3
  18. package/dist/graphql/objects/index.d.ts +3 -3
  19. package/dist/index.cjs +783 -265
  20. package/dist/index.d.cts +6 -6
  21. package/dist/index.d.ts +6 -6
  22. package/dist/index.js +5 -5
  23. package/dist/index.node.cjs +783 -265
  24. package/dist/index.node.d.cts +6 -6
  25. package/dist/index.node.d.ts +6 -6
  26. package/dist/index.node.js +4 -4
  27. package/dist/{logging-BOAzMqpM.d.cts → logging-BfTyKwqb.d.cts} +3 -3
  28. package/dist/{logging-Bt_WdZbu.d.ts → logging-CaNpBgiE.d.ts} +3 -3
  29. package/dist/native/index.cjs +782 -264
  30. package/dist/native/index.d.cts +70 -11
  31. package/dist/native/index.d.ts +70 -11
  32. package/dist/native/index.js +686 -172
  33. package/dist/proto/spark.cjs +499 -8
  34. package/dist/proto/spark.d.cts +1 -1
  35. package/dist/proto/spark.d.ts +1 -1
  36. package/dist/proto/spark.js +17 -1
  37. package/dist/proto/spark_token.d.cts +1 -1
  38. package/dist/proto/spark_token.d.ts +1 -1
  39. package/dist/proto/spark_token.js +2 -2
  40. package/dist/{spark-CtGJPkx4.d.cts → spark-C7OG9mGJ.d.cts} +79 -2
  41. package/dist/{spark-CtGJPkx4.d.ts → spark-C7OG9mGJ.d.ts} +79 -2
  42. package/dist/{spark-wallet-Cp3yv6cK.d.ts → spark-wallet-D0Df_P_x.d.ts} +26 -13
  43. package/dist/{spark-wallet-yc2KhsVY.d.cts → spark-wallet-Dvh1BLP6.d.cts} +26 -13
  44. package/dist/{spark-wallet.node-D0Qw5Wb4.d.cts → spark-wallet.node-B3V8_fgw.d.cts} +1 -1
  45. package/dist/{spark-wallet.node-D4IovOHu.d.ts → spark-wallet.node-bGmy8-T8.d.ts} +1 -1
  46. package/dist/tests/test-utils.cjs +573 -66
  47. package/dist/tests/test-utils.d.cts +4 -4
  48. package/dist/tests/test-utils.d.ts +4 -4
  49. package/dist/tests/test-utils.js +5 -5
  50. package/dist/{token-transactions-CwhlOgIP.d.cts → token-transactions-D1ta-sHH.d.cts} +2 -2
  51. package/dist/{token-transactions-0nmR9mQO.d.ts → token-transactions-DINiKBzd.d.ts} +2 -2
  52. package/dist/types/index.cjs +492 -9
  53. package/dist/types/index.d.cts +2 -2
  54. package/dist/types/index.d.ts +2 -2
  55. package/dist/types/index.js +2 -2
  56. package/package.json +3 -3
  57. package/src/proto/common.ts +1 -1
  58. package/src/proto/google/protobuf/descriptor.ts +4 -10
  59. package/src/proto/google/protobuf/duration.ts +1 -1
  60. package/src/proto/google/protobuf/empty.ts +1 -1
  61. package/src/proto/google/protobuf/timestamp.ts +1 -1
  62. package/src/proto/mock.ts +1 -1
  63. package/src/proto/spark.ts +593 -3
  64. package/src/proto/spark_authn.ts +1 -1
  65. package/src/proto/spark_token.ts +1 -1
  66. package/src/proto/validate/validate.ts +27 -79
  67. package/src/services/deposit.ts +55 -3
  68. package/src/services/lightning.ts +2 -2
  69. package/src/services/signing.ts +1 -1
  70. package/src/services/token-transactions.ts +2 -5
  71. package/src/services/transfer.ts +2 -28
  72. package/src/signer/signer.ts +2 -2
  73. package/src/spark-wallet/proto-descriptors.ts +22 -0
  74. package/src/spark-wallet/proto-hash.ts +743 -0
  75. package/src/spark-wallet/proto-reflection.ts +193 -0
  76. package/src/spark-wallet/spark-wallet.ts +95 -57
  77. package/src/spark_descriptors.pb +0 -0
  78. package/src/tests/address.test.ts +10 -10
  79. package/src/tests/bitcoin.test.ts +2 -2
  80. package/src/tests/bufbuild-reflection.test.ts +151 -0
  81. package/src/tests/cross-language-hash.test.ts +79 -0
  82. package/src/tests/integration/address.test.ts +3 -12
  83. package/src/tests/integration/coop-exit.test.ts +1 -1
  84. package/src/tests/integration/lightning.test.ts +1 -1
  85. package/src/tests/integration/ssp/static_deposit.test.ts +128 -1
  86. package/src/tests/integration/static_deposit.test.ts +26 -0
  87. package/src/tests/integration/swap.test.ts +1 -1
  88. package/src/tests/integration/transfer.test.ts +1 -129
  89. package/src/tests/integration/wallet.test.ts +7 -7
  90. package/src/tests/integration/watchtower.test.ts +1 -1
  91. package/src/tests/token-hashing.test.ts +3 -6
  92. package/src/tests/token-outputs.test.ts +3 -3
  93. package/src/tests/utils/test-faucet.ts +2 -2
  94. package/src/types/sdk-types.ts +1 -1
  95. package/src/utils/adaptor-signature.ts +1 -1
  96. package/src/utils/address.ts +1 -1
  97. package/src/utils/bitcoin.ts +1 -5
  98. package/src/utils/keys.ts +1 -1
  99. package/src/utils/secret-sharing.ts +1 -1
  100. package/src/utils/token-transactions.ts +1 -2
  101. package/src/utils/transfer_package.ts +1 -1
  102. package/src/utils/unilateral-exit.ts +1 -1
@@ -0,0 +1,743 @@
1
+ /**
2
+ * TypeScript implementation of protoreflecthash algorithm for cross-language compatibility.
3
+ * This implements the same objecthash algorithm used by github.com/stackb/protoreflecthash
4
+ * to ensure identical hashes for the same proto messages across Go and JavaScript.
5
+ */
6
+
7
+ import crypto from "crypto";
8
+ import { getFieldNumbers, getFieldMeta } from "./proto-reflection.js";
9
+
10
+ // ObjectHash type identifiers - must match protoreflecthash constants
11
+ const BOOL_IDENTIFIER = "b";
12
+ const MAP_IDENTIFIER = "d";
13
+ const FLOAT_IDENTIFIER = "f";
14
+ const INT_IDENTIFIER = "i";
15
+ const LIST_IDENTIFIER = "l";
16
+ const BYTE_IDENTIFIER = "r";
17
+ const UNICODE_IDENTIFIER = "u";
18
+
19
+ class SkipFieldError extends Error {
20
+ constructor() {
21
+ super("skip field");
22
+ }
23
+ }
24
+
25
+ function isGoogleProtobufValueNull(value: any): boolean {
26
+ if (value == null) return true;
27
+ if (typeof value === "object" && "$case" in value) {
28
+ const c = (value as any).$case as string | undefined;
29
+ return !c || c === "nullValue";
30
+ }
31
+ return false;
32
+ }
33
+
34
+ const TOP_LEVEL_DISALLOWED = new Set<string>([
35
+ "google.protobuf.Value",
36
+ "google.protobuf.ListValue",
37
+ "google.protobuf.BoolValue",
38
+ "google.protobuf.Int32Value",
39
+ "google.protobuf.Int64Value",
40
+ "google.protobuf.UInt32Value",
41
+ "google.protobuf.UInt64Value",
42
+ "google.protobuf.FloatValue",
43
+ "google.protobuf.DoubleValue",
44
+ "google.protobuf.StringValue",
45
+ "google.protobuf.BytesValue",
46
+ ]);
47
+
48
+ interface FieldHashEntry {
49
+ number: number;
50
+ khash: Uint8Array;
51
+ vhash: Uint8Array;
52
+ }
53
+
54
+ /**
55
+ * TypeScript implementation of protoreflecthash for cross-language compatibility
56
+ */
57
+ export class ProtoHasher {
58
+ constructor() {}
59
+
60
+ async hashProto(message: any, messageTypeName?: string): Promise<Uint8Array> {
61
+ if (message == null) {
62
+ throw new Error("cannot hash nil or invalid message");
63
+ }
64
+
65
+ // Disallow hashing of top-level scalar/value wrapper types when type info is provided
66
+ if (messageTypeName && TOP_LEVEL_DISALLOWED.has(messageTypeName)) {
67
+ throw new Error(
68
+ `top-level scalar/value types are not hashable; wrap in a parent message field: ${messageTypeName}`,
69
+ );
70
+ }
71
+
72
+ return this.hashMessage(message, messageTypeName);
73
+ }
74
+
75
+ private async hashMessage(
76
+ message: any,
77
+ messageTypeName?: string,
78
+ ): Promise<Uint8Array> {
79
+ // Special-case well-known types used in our SDK
80
+ // google.protobuf.Timestamp is represented as JS Date (ts-proto default)
81
+ if (message instanceof Date) {
82
+ const millis = message.getTime();
83
+ const secondsOverride = (message as any).__pbSeconds;
84
+ const nanosOverride = (message as any).__pbNanos;
85
+ const seconds =
86
+ typeof secondsOverride === "number"
87
+ ? Math.floor(secondsOverride)
88
+ : Math.floor(millis / 1000);
89
+ const nanos =
90
+ typeof nanosOverride === "number"
91
+ ? Math.floor(nanosOverride)
92
+ : (millis % 1000) * 1_000_000;
93
+
94
+ const secHash = this.hashInt64(seconds);
95
+ const nanoHash = this.hashInt64(nanos);
96
+
97
+ const buffer = new Uint8Array(secHash.length + nanoHash.length);
98
+ buffer.set(secHash, 0);
99
+ buffer.set(nanoHash, secHash.length);
100
+
101
+ return this.hash(LIST_IDENTIFIER, buffer);
102
+ }
103
+
104
+ if (message == null) {
105
+ throw new Error("cannot hash nil message");
106
+ }
107
+
108
+ // Get the message fields using provided message type name for descriptor-based field numbers
109
+ const fields = this.getMessageFields(message, messageTypeName);
110
+
111
+ const fieldHashes: FieldHashEntry[] = [];
112
+
113
+ // Hash each field that is present
114
+ for (const [fieldNumber, fieldInfo] of Object.entries(fields)) {
115
+ if (!fieldInfo?.name) {
116
+ continue;
117
+ }
118
+ const fieldName = fieldInfo.name;
119
+ const fieldType = fieldInfo.type;
120
+
121
+ // Resolve value: support oneof extraction carrying a concrete value
122
+ const resolvedValue =
123
+ (fieldInfo as any).value !== undefined
124
+ ? (fieldInfo as any).value
125
+ : message[fieldName];
126
+
127
+ // Check if field is present (has a value)
128
+ if (this.isDefault(resolvedValue, fieldType)) {
129
+ continue;
130
+ }
131
+
132
+ const fieldValue = resolvedValue;
133
+ const khash = await this.hashFieldKey(parseInt(fieldNumber), fieldName);
134
+
135
+ let vhash: Uint8Array | null = null;
136
+ try {
137
+ vhash = await this.hashFieldValue(fieldType, fieldValue);
138
+ } catch (err) {
139
+ if (err instanceof SkipFieldError) {
140
+ // Omit this field entirely
141
+ continue;
142
+ }
143
+ throw err;
144
+ }
145
+
146
+ fieldHashes.push({
147
+ number: parseInt(fieldNumber),
148
+ khash,
149
+ vhash,
150
+ });
151
+ }
152
+
153
+ // Sort by field number for deterministic ordering
154
+ fieldHashes.sort((a, b) => a.number - b.number);
155
+
156
+ // Concatenate all field hashes
157
+ const totalLength = fieldHashes.reduce(
158
+ (sum, fh) => sum + fh.khash.length + fh.vhash.length,
159
+ 0,
160
+ );
161
+ const buffer = new Uint8Array(totalLength);
162
+ let offset = 0;
163
+
164
+ for (const fh of fieldHashes) {
165
+ buffer.set(fh.khash, offset);
166
+ offset += fh.khash.length;
167
+ buffer.set(fh.vhash, offset);
168
+ offset += fh.vhash.length;
169
+ }
170
+
171
+ // Always use map identifier
172
+ const identifier = MAP_IDENTIFIER;
173
+
174
+ return this.hash(identifier, buffer);
175
+ }
176
+
177
+ private async hashFieldKey(
178
+ fieldNumber: number,
179
+ fieldName: string,
180
+ ): Promise<Uint8Array> {
181
+ return this.hashInt64(fieldNumber);
182
+ }
183
+
184
+ private async hashFieldValue(
185
+ fieldType: any,
186
+ value: any,
187
+ ): Promise<Uint8Array> {
188
+ if (Array.isArray(value)) {
189
+ return this.hashList(fieldType.elementType || fieldType, value);
190
+ }
191
+
192
+ if (this.isMapType(fieldType)) {
193
+ return this.hashMap(fieldType, value);
194
+ }
195
+
196
+ return this.hashValue(fieldType, value);
197
+ }
198
+
199
+ private async hashValue(fieldType: any, value: any): Promise<Uint8Array> {
200
+ const typeName = this.getTypeName(fieldType);
201
+
202
+ switch (typeName) {
203
+ case "bool":
204
+ return this.hashBool(value);
205
+ case "int32":
206
+ case "int64":
207
+ case "sint32":
208
+ case "sint64":
209
+ case "sfixed32":
210
+ case "sfixed64":
211
+ return this.hashInt64(value);
212
+ case "uint32":
213
+ case "uint64":
214
+ case "fixed32":
215
+ case "fixed64":
216
+ return this.hashUint64(value);
217
+ case "float":
218
+ case "double":
219
+ return this.hashFloat(value);
220
+ case "string":
221
+ return this.hashUnicode(value);
222
+ case "bytes":
223
+ return this.hashBytes(value);
224
+ case "message":
225
+ if (fieldType && typeof fieldType === "object" && fieldType.typeName) {
226
+ return this.hashMessage(value, fieldType.typeName);
227
+ }
228
+ return this.hashMessage(value);
229
+ case "oneof":
230
+ return this.hashOneof(value);
231
+ default:
232
+ // Handle enums as integers
233
+ if (typeof value === "number") {
234
+ return this.hashInt64(value);
235
+ }
236
+ throw new Error(`Unsupported field type: ${typeName}`);
237
+ }
238
+ }
239
+
240
+ private async hashOneof(oneofValue: any): Promise<Uint8Array> {
241
+ // For protoc-gen-ts_proto generated types, oneof is represented as:
242
+ // { $case: "fieldName", fieldName: actualValue }
243
+ if (oneofValue && typeof oneofValue === "object" && "$case" in oneofValue) {
244
+ const activeCase = oneofValue.$case;
245
+ const activeValue = oneofValue[activeCase];
246
+
247
+ if (activeValue !== undefined) {
248
+ // Hash the active field's value as a message field
249
+ return this.hashMessage(activeValue);
250
+ }
251
+ }
252
+
253
+ // If no active case, this oneof is effectively unset and should not be hashed here.
254
+ // Callers shouldn't pass unpopulated oneofs to hashing; treat as invalid input.
255
+ throw new Error("invalid oneof: no active value");
256
+ }
257
+
258
+ private async hashList(elementType: any, list: any[]): Promise<Uint8Array> {
259
+ // Empty list is default-equivalent: skip this field entirely
260
+ if (list.length === 0) {
261
+ throw new SkipFieldError();
262
+ }
263
+
264
+ const hashes: Uint8Array[] = [];
265
+
266
+ // If element type is google.protobuf.Value, disallow null elements
267
+ const isValueList =
268
+ typeof elementType === "object" &&
269
+ (elementType as any)?.typeName === "google.protobuf.Value";
270
+
271
+ for (const item of list) {
272
+ if (isValueList && isGoogleProtobufValueNull(item)) {
273
+ throw new Error("cannot hash nil value");
274
+ }
275
+ const itemHash = await this.hashValue(elementType, item);
276
+ hashes.push(itemHash);
277
+ }
278
+
279
+ // Concatenate all item hashes
280
+ const totalLength = hashes.reduce((sum, h) => sum + h.length, 0);
281
+ const buffer = new Uint8Array(totalLength);
282
+ let offset = 0;
283
+
284
+ for (const h of hashes) {
285
+ buffer.set(h, offset);
286
+ offset += h.length;
287
+ }
288
+
289
+ return this.hash(LIST_IDENTIFIER, buffer);
290
+ }
291
+
292
+ private async hashMap(
293
+ fieldType: any,
294
+ map: Record<string, any>,
295
+ ): Promise<Uint8Array> {
296
+ const entries: Array<{ khash: Uint8Array; vhash: Uint8Array }> = [];
297
+
298
+ const valueIsGoogleValue =
299
+ fieldType?.valueType &&
300
+ fieldType.valueType.typeName === "google.protobuf.Value";
301
+
302
+ for (const [key, value] of Object.entries(map)) {
303
+ // Skip entries where the VALUE is google.protobuf.Value set to null
304
+ if (valueIsGoogleValue && isGoogleProtobufValueNull(value)) {
305
+ continue;
306
+ }
307
+
308
+ const khash = await this.hashValue(fieldType.keyType, key);
309
+
310
+ let vhash: Uint8Array;
311
+ try {
312
+ vhash = await this.hashValue(fieldType.valueType, value);
313
+ } catch (err) {
314
+ // If nested hashing signaled default-equivalence (e.g., empty list), skip this entry
315
+ if (err instanceof SkipFieldError) {
316
+ continue;
317
+ }
318
+ throw err;
319
+ }
320
+
321
+ entries.push({ khash, vhash });
322
+ }
323
+
324
+ // If, after skipping, no effective entries remain, skip the map field entirely
325
+ if (entries.length === 0) {
326
+ throw new SkipFieldError();
327
+ }
328
+
329
+ // Sort by key hash for deterministic ordering
330
+ entries.sort((a, b) => this.compareBytes(a.khash, b.khash));
331
+
332
+ // Concatenate all entry hashes
333
+ const totalLength = entries.reduce(
334
+ (sum, e) => sum + e.khash.length + e.vhash.length,
335
+ 0,
336
+ );
337
+ const buffer = new Uint8Array(totalLength);
338
+ let offset = 0;
339
+
340
+ for (const entry of entries) {
341
+ buffer.set(entry.khash, offset);
342
+ offset += entry.khash.length;
343
+ buffer.set(entry.vhash, offset);
344
+ offset += entry.vhash.length;
345
+ }
346
+
347
+ return this.hash(MAP_IDENTIFIER, buffer);
348
+ }
349
+
350
+ // Basic hash functions following objecthash spec
351
+ private hashBool(value: boolean): Uint8Array {
352
+ const bytes = value ? new Uint8Array([49]) : new Uint8Array([48]); // "1" or "0"
353
+ return this.hash(BOOL_IDENTIFIER, bytes);
354
+ }
355
+
356
+ private hashInt64(value: number | bigint): Uint8Array {
357
+ const b = this.encodeInt64BigEndian(value);
358
+ return this.hash(INT_IDENTIFIER, b);
359
+ }
360
+
361
+ private hashUint64(value: number | bigint): Uint8Array {
362
+ const b = this.encodeUint64BigEndian(value);
363
+ return this.hash(INT_IDENTIFIER, b);
364
+ }
365
+
366
+ private hashFloat(value: number): Uint8Array {
367
+ // Normalize -0.0 to +0.0
368
+ let f = Object.is(value, -0) ? 0 : value;
369
+ // Use a canonical NaN representation by forcing to NaN when NaN
370
+ if (Number.isNaN(f)) {
371
+ f = Number.NaN;
372
+ }
373
+
374
+ const buf = new ArrayBuffer(8);
375
+ const view = new DataView(buf);
376
+ view.setFloat64(0, f, false); // big-endian
377
+ return this.hash(FLOAT_IDENTIFIER, new Uint8Array(buf));
378
+ }
379
+
380
+ private hashUnicode(value: string): Uint8Array {
381
+ return this.hash(UNICODE_IDENTIFIER, new TextEncoder().encode(value));
382
+ }
383
+
384
+ private hashBytes(value: Uint8Array | ArrayBuffer | number[]): Uint8Array {
385
+ let bytes: Uint8Array;
386
+ if (value instanceof Uint8Array) {
387
+ bytes = value;
388
+ } else if (value instanceof ArrayBuffer) {
389
+ bytes = new Uint8Array(value);
390
+ } else {
391
+ bytes = new Uint8Array(value);
392
+ }
393
+ return this.hash(BYTE_IDENTIFIER, bytes);
394
+ }
395
+
396
+ // Note: No NIL hashing in JS. Nil/undefined should never be hashed and will error.
397
+
398
+ private hash(typeIdentifier: string, data: Uint8Array): Uint8Array {
399
+ const hasher = crypto.createHash("sha256");
400
+ hasher.update(typeIdentifier, "utf8");
401
+ hasher.update(data);
402
+ return new Uint8Array(hasher.digest());
403
+ }
404
+
405
+ // Generic protobuf field introspection using real protobuf field numbers
406
+ private getMessageFields(
407
+ message: any,
408
+ messageTypeName?: string,
409
+ ): Record<number, any> {
410
+ const fields: Record<number, any> = {};
411
+
412
+ // Use descriptor-based numbers when a message type name is provided.
413
+ const reflectionNumbers = messageTypeName
414
+ ? getFieldNumbers(messageTypeName)
415
+ : {};
416
+ const fieldMeta = messageTypeName ? getFieldMeta(messageTypeName) : {};
417
+
418
+ function camelToSnake(name: string): string {
419
+ return name
420
+ .replace(/([A-Z])/g, "_$1")
421
+ .replace(/^_/, "")
422
+ .toLowerCase();
423
+ }
424
+
425
+ // Track used field numbers to avoid conflicts
426
+ const usedTags = new Set<number>();
427
+
428
+ // Process all enumerable properties
429
+ const allKeys = Object.getOwnPropertyNames(message).concat(
430
+ Object.keys(message),
431
+ );
432
+ const uniqueKeys = [...new Set(allKeys)];
433
+
434
+ for (const fieldName of uniqueKeys) {
435
+ const value = message[fieldName];
436
+
437
+ // Handle oneof fields specially
438
+ if (this.isOneofField(value)) {
439
+ const oneofInfo = this.extractOneofField(
440
+ fieldName,
441
+ value,
442
+ reflectionNumbers as any,
443
+ );
444
+ if (oneofInfo) {
445
+ const { actualFieldNumber, actualFieldName, actualValue } = oneofInfo;
446
+
447
+ usedTags.add(actualFieldNumber);
448
+
449
+ const snakeCase = camelToSnake(actualFieldName);
450
+ const nestedTypeName = (fieldMeta as any)[snakeCase]?.typeName as
451
+ | string
452
+ | undefined;
453
+ const inferred = this.inferFieldType(actualValue);
454
+ const typeWithHint = nestedTypeName
455
+ ? { type: "message", typeName: nestedTypeName }
456
+ : inferred;
457
+ fields[actualFieldNumber] = {
458
+ name: actualFieldName,
459
+ type: typeWithHint,
460
+ value: actualValue,
461
+ };
462
+ }
463
+ continue;
464
+ }
465
+
466
+ // Get the canonical protobuf field number for this field using reflection only
467
+ const snake = camelToSnake(fieldName);
468
+ const canonicalTag =
469
+ reflectionNumbers && snake in (reflectionNumbers as any)
470
+ ? ((reflectionNumbers as any)[snake] as number)
471
+ : undefined;
472
+ if (canonicalTag === undefined) {
473
+ throw new Error(
474
+ `Unknown field '${fieldName}' for message type '${messageTypeName ?? "<unknown>"}'`,
475
+ );
476
+ }
477
+ const fieldNumber = canonicalTag;
478
+
479
+ usedTags.add(fieldNumber);
480
+
481
+ if (value !== undefined) {
482
+ const snakeCase = camelToSnake(fieldName);
483
+ const nestedTypeName = (fieldMeta as any)[snakeCase]?.typeName as
484
+ | string
485
+ | undefined;
486
+ const inferred = this.inferFieldType(value);
487
+ const typeWithHint = nestedTypeName
488
+ ? { type: "message", typeName: nestedTypeName }
489
+ : inferred;
490
+ fields[fieldNumber] = {
491
+ name: fieldName,
492
+ type: typeWithHint,
493
+ value,
494
+ };
495
+ }
496
+ }
497
+
498
+ if (Object.keys(fields).length === 0) {
499
+ const dbgReflectionKeys = Object.keys(reflectionNumbers || {});
500
+ const dbgAllKeys = Object.getOwnPropertyNames(message).concat(
501
+ Object.keys(message),
502
+ );
503
+ console.log("proto-hash: no fields found", {
504
+ messageTypeName,
505
+ reflectionKeys: dbgReflectionKeys,
506
+ messageKeys: Array.from(new Set(dbgAllKeys)),
507
+ });
508
+ throw new Error(
509
+ "No fields found in message (missing or invalid messageTypeName)",
510
+ );
511
+ }
512
+
513
+ return fields;
514
+ }
515
+
516
+ private isOneofField(value: any): boolean {
517
+ return (
518
+ value &&
519
+ typeof value === "object" &&
520
+ "$case" in value &&
521
+ typeof (value as any).$case === "string"
522
+ );
523
+ }
524
+
525
+ private extractOneofField(
526
+ fieldName: string,
527
+ value: any,
528
+ reflectionNumbers: Record<string, number>,
529
+ ): {
530
+ actualFieldNumber: number;
531
+ actualFieldName: string;
532
+ actualValue: any;
533
+ } | null {
534
+ if (!value || !("$case" in value)) return null;
535
+
536
+ const actualFieldName = (value as any).$case as string;
537
+ const actualValue = (value as any)[actualFieldName];
538
+ const snake = actualFieldName
539
+ .replace(/([A-Z])/g, "_$1")
540
+ .replace(/^_/, "")
541
+ .toLowerCase();
542
+ const actualFieldNumber = (reflectionNumbers as any)[snake] as
543
+ | number
544
+ | undefined;
545
+ if (!actualFieldNumber) return null;
546
+
547
+ return {
548
+ actualFieldNumber,
549
+ actualFieldName,
550
+ actualValue,
551
+ };
552
+ }
553
+
554
+ private inferFieldType(value: any): string {
555
+ if (typeof value === "boolean") return "bool";
556
+ if (typeof value === "number") {
557
+ if (Number.isInteger(value) && value >= 0) {
558
+ if (value <= 0xffffffff) {
559
+ return "uint32";
560
+ } else {
561
+ return "uint64";
562
+ }
563
+ }
564
+ return "int64";
565
+ }
566
+ if (typeof value === "bigint") return "uint64";
567
+ if (typeof value === "string") return "string";
568
+ if (value instanceof Uint8Array) return "bytes";
569
+ if (Array.isArray(value)) return "list";
570
+ if (typeof value === "object" && value !== null) return "message";
571
+ return "unknown";
572
+ }
573
+
574
+ private isDefault(value: any, fieldType: any): boolean {
575
+ if (value == null) {
576
+ return true;
577
+ }
578
+
579
+ if (
580
+ this.getTypeName(fieldType) === "message" &&
581
+ (fieldType as any).typeName === "google.protobuf.Value"
582
+ ) {
583
+ if (isGoogleProtobufValueNull(value)) return true;
584
+ }
585
+
586
+ if (Array.isArray(value)) {
587
+ return value.length === 0;
588
+ }
589
+
590
+ if (this.isMapType(fieldType)) {
591
+ return Object.keys(value).length === 0;
592
+ }
593
+
594
+ switch (this.getTypeName(fieldType)) {
595
+ case "bool":
596
+ return value === false;
597
+ case "int32":
598
+ case "int64":
599
+ case "sint32":
600
+ case "sint64":
601
+ case "sfixed32":
602
+ case "sfixed64":
603
+ case "uint32":
604
+ case "uint64":
605
+ case "fixed32":
606
+ case "fixed64":
607
+ return value === 0 || value === 0n;
608
+ case "float":
609
+ case "double":
610
+ return value === 0;
611
+ case "string":
612
+ return value.length === 0;
613
+ case "bytes":
614
+ return value.length === 0;
615
+ default:
616
+ if (typeof value === "number") {
617
+ return value === 0;
618
+ }
619
+ }
620
+ return false;
621
+ }
622
+
623
+ private getTypeName(fieldType: any): string {
624
+ if (typeof fieldType === "string") {
625
+ return fieldType;
626
+ }
627
+ if (fieldType.type) {
628
+ return fieldType.type;
629
+ }
630
+ if ((fieldType as any).resolvedType) {
631
+ return "message";
632
+ }
633
+ return "unknown";
634
+ }
635
+
636
+ private getMessageFullName(message: any): string {
637
+ const constructorName = message?.constructor?.name;
638
+ if (constructorName && constructorName !== "Object") {
639
+ return constructorName;
640
+ }
641
+ return "protobuf.Message";
642
+ }
643
+
644
+ private isMapType(fieldType: any): boolean {
645
+ return fieldType.map === true || (fieldType.keyType && fieldType.valueType);
646
+ }
647
+
648
+ private isProto3(message: any): boolean {
649
+ return true; // Assume proto3 for now
650
+ }
651
+
652
+ private compareBytes(a: Uint8Array, b: Uint8Array): number {
653
+ const minLength = Math.min(a.length, b.length);
654
+ for (let i = 0; i < minLength; i++) {
655
+ const aVal = a[i];
656
+ const bVal = b[i];
657
+ if (aVal !== undefined && bVal !== undefined && aVal !== bVal) {
658
+ return aVal - bVal;
659
+ }
660
+ }
661
+ return a.length - b.length;
662
+ }
663
+
664
+ private floatNormalize(originalFloat: number): string {
665
+ if (originalFloat === 0) {
666
+ return "+0:";
667
+ }
668
+ let f = originalFloat;
669
+ let s = "+";
670
+ if (f < 0) {
671
+ s = "-";
672
+ f = -f;
673
+ }
674
+ let e = 0;
675
+ while (f > 1) {
676
+ f /= 2;
677
+ e++;
678
+ }
679
+ while (f <= 0.5) {
680
+ f *= 2;
681
+ e--;
682
+ }
683
+ s += `${e}:`;
684
+ if (f > 1 || f <= 0.5) {
685
+ throw new Error(`Could not normalize float: ${originalFloat}`);
686
+ }
687
+ while (f !== 0) {
688
+ if (f >= 1) {
689
+ s += "1";
690
+ f--;
691
+ } else {
692
+ s += "0";
693
+ }
694
+ if (f >= 1) {
695
+ throw new Error(`Could not normalize float: ${originalFloat}`);
696
+ }
697
+ if (s.length >= 1000) {
698
+ throw new Error(`Could not normalize float: ${originalFloat}`);
699
+ }
700
+ f *= 2;
701
+ }
702
+ return s;
703
+ }
704
+
705
+ private encodeUint64BigEndian(value: number | bigint): Uint8Array {
706
+ let v = typeof value === "bigint" ? value : BigInt(value);
707
+ const out = new Uint8Array(8);
708
+ for (let i = 7; i >= 0; i--) {
709
+ out[i] = Number(v & 0xffn);
710
+ v >>= 8n;
711
+ }
712
+ return out;
713
+ }
714
+
715
+ private encodeInt64BigEndian(value: number | bigint): Uint8Array {
716
+ let v = typeof value === "bigint" ? value : BigInt(value);
717
+ const mask = (1n << 64n) - 1n;
718
+ if (v < 0) {
719
+ v = v & mask;
720
+ }
721
+ const out = new Uint8Array(8);
722
+ for (let i = 7; i >= 0; i--) {
723
+ out[i] = Number(v & 0xffn);
724
+ v >>= 8n;
725
+ }
726
+ return out;
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Create a new ProtoHasher instance
732
+ */
733
+ export function createProtoHasher(): ProtoHasher {
734
+ return new ProtoHasher();
735
+ }
736
+
737
+ /**
738
+ * Hash a protobuf message with default options
739
+ */
740
+ export async function hashProto(message: any): Promise<Uint8Array> {
741
+ const hasher = new ProtoHasher();
742
+ return hasher.hashProto(message);
743
+ }