@buildonspark/spark-sdk 0.1.44 → 0.1.46

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 (143) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{RequestLightningSendInput-BxbCtwpV.d.cts → RequestLightningSendInput-2cSh_In4.d.cts} +1 -1
  3. package/dist/{RequestLightningSendInput-RGel43ks.d.ts → RequestLightningSendInput-CN6BNg_g.d.ts} +1 -1
  4. package/dist/address/index.cjs +2 -2
  5. package/dist/address/index.d.cts +2 -2
  6. package/dist/address/index.d.ts +2 -2
  7. package/dist/address/index.js +2 -2
  8. package/dist/{chunk-EKFD62HN.js → chunk-4EMV7HHW.js} +2 -1
  9. package/dist/{chunk-4Q2ZDYYU.js → chunk-BGGEVUJK.js} +1157 -208
  10. package/dist/{chunk-CIZNCBKE.js → chunk-C2S227QR.js} +648 -45
  11. package/dist/{chunk-WPTRVD2V.js → chunk-DXR2PXJU.js} +15 -15
  12. package/dist/{chunk-NBCNYDWJ.js → chunk-HHNQ3ZHC.js} +2 -2
  13. package/dist/{chunk-DAXGVPVM.js → chunk-HSCLBJEL.js} +2 -2
  14. package/dist/{chunk-6AFUC5M2.js → chunk-HWJWKEIU.js} +8 -2
  15. package/dist/{chunk-A2ZLMH6I.js → chunk-JB64OQES.js} +259 -327
  16. package/dist/{chunk-KEKGSH7B.js → chunk-KMUMFYFX.js} +3 -3
  17. package/dist/chunk-LHRD2WT6.js +2374 -0
  18. package/dist/{chunk-HTMXTJRK.js → chunk-N5VZVCGJ.js} +4 -4
  19. package/dist/{chunk-SQKXGAIR.js → chunk-NTFKFRQ2.js} +1 -1
  20. package/dist/{chunk-K4BJARWM.js → chunk-OBFKIEMP.js} +1 -1
  21. package/dist/{chunk-UBT6EDVJ.js → chunk-OFCJFZ4I.js} +1 -1
  22. package/dist/{chunk-XX4RRWOX.js → chunk-UXDODSDT.js} +8 -10
  23. package/dist/graphql/objects/index.d.cts +5 -4
  24. package/dist/graphql/objects/index.d.ts +5 -4
  25. package/dist/index-CKL5DodV.d.cts +214 -0
  26. package/dist/index-COm59SPw.d.ts +214 -0
  27. package/dist/index.cjs +4026 -1315
  28. package/dist/index.d.cts +764 -19
  29. package/dist/index.d.ts +764 -19
  30. package/dist/index.js +23 -27
  31. package/dist/index.node.cjs +4026 -1319
  32. package/dist/index.node.d.cts +10 -8
  33. package/dist/index.node.d.ts +10 -8
  34. package/dist/index.node.js +23 -31
  35. package/dist/native/index.cjs +4027 -1316
  36. package/dist/native/index.d.cts +281 -85
  37. package/dist/native/index.d.ts +281 -85
  38. package/dist/native/index.js +4018 -1307
  39. package/dist/{network-CfxLnaot.d.cts → network-Css46DAz.d.cts} +1 -1
  40. package/dist/{network-CroCOQ0B.d.ts → network-hynb7iTZ.d.ts} +1 -1
  41. package/dist/proto/lrc20.cjs +222 -19
  42. package/dist/proto/lrc20.d.cts +1 -1
  43. package/dist/proto/lrc20.d.ts +1 -1
  44. package/dist/proto/lrc20.js +2 -2
  45. package/dist/proto/spark.cjs +1154 -205
  46. package/dist/proto/spark.d.cts +1 -1
  47. package/dist/proto/spark.d.ts +1 -1
  48. package/dist/proto/spark.js +3 -1
  49. package/dist/proto/spark_token.cjs +1377 -58
  50. package/dist/proto/spark_token.d.cts +153 -15
  51. package/dist/proto/spark_token.d.ts +153 -15
  52. package/dist/proto/spark_token.js +40 -4
  53. package/dist/{sdk-types-CTbTdDbE.d.ts → sdk-types-CKBsylfW.d.ts} +1 -1
  54. package/dist/{sdk-types-BeCBoozO.d.cts → sdk-types-Ct8xmN7l.d.cts} +1 -1
  55. package/dist/services/config.cjs +2 -2
  56. package/dist/services/config.d.cts +5 -4
  57. package/dist/services/config.d.ts +5 -4
  58. package/dist/services/config.js +6 -6
  59. package/dist/services/connection.cjs +2438 -262
  60. package/dist/services/connection.d.cts +5 -4
  61. package/dist/services/connection.d.ts +5 -4
  62. package/dist/services/connection.js +4 -4
  63. package/dist/services/index.cjs +5937 -3154
  64. package/dist/services/index.d.cts +7 -6
  65. package/dist/services/index.d.ts +7 -6
  66. package/dist/services/index.js +17 -15
  67. package/dist/services/lrc-connection.cjs +223 -20
  68. package/dist/services/lrc-connection.d.cts +5 -4
  69. package/dist/services/lrc-connection.d.ts +5 -4
  70. package/dist/services/lrc-connection.js +4 -4
  71. package/dist/services/token-transactions.cjs +840 -236
  72. package/dist/services/token-transactions.d.cts +25 -7
  73. package/dist/services/token-transactions.d.ts +25 -7
  74. package/dist/services/token-transactions.js +5 -4
  75. package/dist/services/wallet-config.cjs +3 -1
  76. package/dist/services/wallet-config.d.cts +7 -5
  77. package/dist/services/wallet-config.d.ts +7 -5
  78. package/dist/services/wallet-config.js +3 -1
  79. package/dist/signer/signer.cjs +1 -1
  80. package/dist/signer/signer.d.cts +3 -2
  81. package/dist/signer/signer.d.ts +3 -2
  82. package/dist/signer/signer.js +2 -2
  83. package/dist/{signer-D7vfYik9.d.ts → signer-BP6F__oR.d.cts} +2 -6
  84. package/dist/{signer-DaY8c60s.d.cts → signer-BVZJXcq7.d.ts} +2 -6
  85. package/dist/{spark-C4ZrsgjC.d.cts → spark-DbzGfse6.d.cts} +93 -15
  86. package/dist/{spark-C4ZrsgjC.d.ts → spark-DbzGfse6.d.ts} +93 -15
  87. package/dist/spark_bindings/native/index.cjs +183 -0
  88. package/dist/spark_bindings/native/index.d.cts +14 -0
  89. package/dist/spark_bindings/native/index.d.ts +14 -0
  90. package/dist/spark_bindings/native/index.js +141 -0
  91. package/dist/spark_bindings/wasm/index.cjs +1093 -0
  92. package/dist/spark_bindings/wasm/index.d.cts +47 -0
  93. package/dist/spark_bindings/wasm/index.d.ts +47 -0
  94. package/dist/{chunk-K4C4W5FC.js → spark_bindings/wasm/index.js} +7 -6
  95. package/dist/types/index.cjs +1156 -208
  96. package/dist/types/index.d.cts +5 -4
  97. package/dist/types/index.d.ts +5 -4
  98. package/dist/types/index.js +2 -2
  99. package/dist/types-C-Rp0Oo7.d.cts +46 -0
  100. package/dist/types-C-Rp0Oo7.d.ts +46 -0
  101. package/dist/utils/index.cjs +65 -13
  102. package/dist/utils/index.d.cts +14 -134
  103. package/dist/utils/index.d.ts +14 -134
  104. package/dist/utils/index.js +13 -13
  105. package/package.json +22 -2
  106. package/src/index.node.ts +0 -1
  107. package/src/index.ts +0 -1
  108. package/src/native/index.ts +1 -2
  109. package/src/proto/common.ts +5 -5
  110. package/src/proto/google/protobuf/descriptor.ts +34 -34
  111. package/src/proto/google/protobuf/duration.ts +2 -2
  112. package/src/proto/google/protobuf/empty.ts +2 -2
  113. package/src/proto/google/protobuf/timestamp.ts +2 -2
  114. package/src/proto/mock.ts +4 -4
  115. package/src/proto/spark.ts +1452 -185
  116. package/src/proto/spark_authn.ts +7 -7
  117. package/src/proto/spark_token.ts +1668 -105
  118. package/src/proto/validate/validate.ts +24 -24
  119. package/src/services/bolt11-spark.ts +62 -187
  120. package/src/services/coop-exit.ts +3 -0
  121. package/src/services/lrc20.ts +1 -1
  122. package/src/services/token-transactions.ts +197 -9
  123. package/src/services/transfer.ts +22 -0
  124. package/src/services/tree-creation.ts +13 -0
  125. package/src/services/wallet-config.ts +2 -2
  126. package/src/spark-wallet/spark-wallet.node.ts +0 -4
  127. package/src/spark-wallet/spark-wallet.ts +76 -108
  128. package/src/spark-wallet/types.ts +39 -3
  129. package/src/tests/bolt11-spark.test.ts +7 -15
  130. package/src/tests/integration/ssp/coop-exit.test.ts +7 -7
  131. package/src/tests/integration/swap.test.ts +453 -433
  132. package/src/tests/integration/transfer.test.ts +261 -248
  133. package/src/tests/token-identifier.test.ts +54 -0
  134. package/src/tests/tokens.test.ts +218 -23
  135. package/src/utils/token-hashing.ts +320 -44
  136. package/src/utils/token-identifier.ts +88 -0
  137. package/src/utils/token-transaction-validation.ts +350 -5
  138. package/src/utils/token-transactions.ts +12 -8
  139. package/src/utils/transaction.ts +0 -6
  140. package/dist/chunk-B3AMIGJG.js +0 -1073
  141. package/dist/index-CZmDdSts.d.cts +0 -829
  142. package/dist/index-ClIRO_3y.d.ts +0 -829
  143. package/dist/wasm-7OWFHDMS.js +0 -21
@@ -3,8 +3,12 @@ import { ValidationError } from "../errors/types.js";
3
3
  import {
4
4
  OperatorSpecificTokenTransactionSignablePayload,
5
5
  TokenTransaction as TokenTransactionV0,
6
+ TokenMintInput as TokenMintInputV0,
6
7
  } from "../proto/spark.js";
7
- import { TokenTransaction } from "../proto/spark_token.js";
8
+ import {
9
+ TokenTransaction,
10
+ TokenTransactionType,
11
+ } from "../proto/spark_token.js";
8
12
 
9
13
  export function hashTokenTransaction(
10
14
  tokenTransaction: TokenTransaction,
@@ -24,7 +28,7 @@ export function hashTokenTransaction(
24
28
  }
25
29
 
26
30
  export function hashTokenTransactionV0(
27
- tokenTransaction: TokenTransactionV0,
31
+ tokenTransaction: TokenTransactionV0 | TokenTransaction,
28
32
  partialHash: boolean = false,
29
33
  ): Uint8Array {
30
34
  if (!tokenTransaction) {
@@ -111,15 +115,30 @@ export function hashTokenTransactionV0(
111
115
  }
112
116
  hashObj.update(issuerPubKey);
113
117
 
114
- if (
115
- tokenTransaction.tokenInputs.mintInput!.issuerProvidedTimestamp != 0
118
+ // Handle both TokenTransactionV0 (with issuerProvidedTimestamp) and TokenTransaction (with clientCreatedTimestamp)
119
+ let timestampValue = 0;
120
+ const mintInput = tokenTransaction.tokenInputs.mintInput!;
121
+
122
+ // Check if this is a TokenTransactionV0 (has issuerProvidedTimestamp)
123
+ if ("issuerProvidedTimestamp" in mintInput) {
124
+ const v0MintInput = mintInput as TokenMintInputV0;
125
+ if (v0MintInput.issuerProvidedTimestamp != 0) {
126
+ timestampValue = v0MintInput.issuerProvidedTimestamp;
127
+ }
128
+ }
129
+ // Check if this is a TokenTransaction (has clientCreatedTimestamp)
130
+ else if (
131
+ "clientCreatedTimestamp" in tokenTransaction &&
132
+ tokenTransaction.clientCreatedTimestamp
116
133
  ) {
134
+ timestampValue = tokenTransaction.clientCreatedTimestamp.getTime();
135
+ }
136
+
137
+ if (timestampValue != 0) {
117
138
  const timestampBytes = new Uint8Array(8);
118
139
  new DataView(timestampBytes.buffer).setBigUint64(
119
140
  0,
120
- BigInt(
121
- tokenTransaction.tokenInputs.mintInput!.issuerProvidedTimestamp,
122
- ),
141
+ BigInt(timestampValue),
123
142
  true, // true for little-endian to match Go implementation
124
143
  );
125
144
  hashObj.update(timestampBytes);
@@ -128,6 +147,108 @@ export function hashTokenTransactionV0(
128
147
  }
129
148
  }
130
149
 
150
+ // Hash create input if a create
151
+ if (tokenTransaction.tokenInputs?.$case === "createInput") {
152
+ const issuerPubKeyHashObj = sha256.create();
153
+ const createInput = tokenTransaction.tokenInputs.createInput!;
154
+
155
+ // Hash issuer public key
156
+ if (
157
+ !createInput.issuerPublicKey ||
158
+ createInput.issuerPublicKey.length === 0
159
+ ) {
160
+ throw new ValidationError("issuer public key cannot be nil or empty", {
161
+ field: "tokenInputs.createInput.issuerPublicKey",
162
+ });
163
+ }
164
+ issuerPubKeyHashObj.update(createInput.issuerPublicKey);
165
+ allHashes.push(issuerPubKeyHashObj.digest());
166
+
167
+ // Hash token name (fixed 20 bytes)
168
+ const tokenNameHashObj = sha256.create();
169
+ if (!createInput.tokenName || createInput.tokenName.length === 0) {
170
+ throw new ValidationError("token name cannot be empty", {
171
+ field: "tokenInputs.createInput.tokenName",
172
+ });
173
+ }
174
+ if (createInput.tokenName.length > 20) {
175
+ throw new ValidationError("token name cannot be longer than 20 bytes", {
176
+ field: "tokenInputs.createInput.tokenName",
177
+ value: createInput.tokenName,
178
+ expectedLength: 20,
179
+ actualLength: createInput.tokenName.length,
180
+ });
181
+ }
182
+ const tokenNameBytes = new Uint8Array(20);
183
+ const tokenNameEncoder = new TextEncoder();
184
+ tokenNameBytes.set(tokenNameEncoder.encode(createInput.tokenName));
185
+ tokenNameHashObj.update(tokenNameBytes);
186
+ allHashes.push(tokenNameHashObj.digest());
187
+
188
+ // Hash token ticker (fixed 6 bytes)
189
+ const tokenTickerHashObj = sha256.create();
190
+ if (!createInput.tokenTicker || createInput.tokenTicker.length === 0) {
191
+ throw new ValidationError("token ticker cannot be empty", {
192
+ field: "tokenInputs.createInput.tokenTicker",
193
+ });
194
+ }
195
+ if (createInput.tokenTicker.length > 6) {
196
+ throw new ValidationError("token ticker cannot be longer than 6 bytes", {
197
+ field: "tokenInputs.createInput.tokenTicker",
198
+ value: createInput.tokenTicker,
199
+ expectedLength: 6,
200
+ actualLength: createInput.tokenTicker.length,
201
+ });
202
+ }
203
+ const tokenTickerBytes = new Uint8Array(6);
204
+ const tokenTickerEncoder = new TextEncoder();
205
+ tokenTickerBytes.set(tokenTickerEncoder.encode(createInput.tokenTicker));
206
+ tokenTickerHashObj.update(tokenTickerBytes);
207
+ allHashes.push(tokenTickerHashObj.digest());
208
+
209
+ // Hash decimals
210
+ const decimalsHashObj = sha256.create();
211
+ const decimalsBytes = new Uint8Array(4);
212
+ new DataView(decimalsBytes.buffer).setUint32(
213
+ 0,
214
+ createInput.decimals,
215
+ false,
216
+ );
217
+ decimalsHashObj.update(decimalsBytes);
218
+ allHashes.push(decimalsHashObj.digest());
219
+
220
+ // Hash max supply (fixed 16 bytes)
221
+ const maxSupplyHashObj = sha256.create();
222
+ if (!createInput.maxSupply) {
223
+ throw new ValidationError("max supply cannot be nil", {
224
+ field: "tokenInputs.createInput.maxSupply",
225
+ });
226
+ }
227
+ if (createInput.maxSupply.length !== 16) {
228
+ throw new ValidationError("max supply must be exactly 16 bytes", {
229
+ field: "tokenInputs.createInput.maxSupply",
230
+ value: createInput.maxSupply,
231
+ expectedLength: 16,
232
+ actualLength: createInput.maxSupply.length,
233
+ });
234
+ }
235
+ maxSupplyHashObj.update(createInput.maxSupply);
236
+ allHashes.push(maxSupplyHashObj.digest());
237
+
238
+ // Hash is freezable
239
+ const isFreezableHashObj = sha256.create();
240
+ const isFreezableByte = new Uint8Array([createInput.isFreezable ? 1 : 0]);
241
+ isFreezableHashObj.update(isFreezableByte);
242
+ allHashes.push(isFreezableHashObj.digest());
243
+
244
+ // Hash creation entity public key (only for final hash)
245
+ const creationEntityHashObj = sha256.create();
246
+ if (!partialHash && createInput.creationEntityPublicKey) {
247
+ creationEntityHashObj.update(createInput.creationEntityPublicKey);
248
+ }
249
+ allHashes.push(creationEntityHashObj.digest());
250
+ }
251
+
131
252
  // Hash token outputs
132
253
  if (!tokenTransaction.tokenOutputs) {
133
254
  throw new ValidationError("token outputs cannot be null", {
@@ -135,7 +256,12 @@ export function hashTokenTransactionV0(
135
256
  });
136
257
  }
137
258
 
138
- if (tokenTransaction.tokenOutputs.length === 0) {
259
+ if (
260
+ tokenTransaction.tokenOutputs.length === 0 &&
261
+ tokenTransaction.tokenInputs?.$case !== "createInput"
262
+ ) {
263
+ // Mint and transfer transactions must have at least one output, but create transactions
264
+ // are allowed to have none (they define metadata only).
139
265
  throw new ValidationError("token outputs cannot be empty", {
140
266
  field: "tokenOutputs",
141
267
  });
@@ -337,7 +463,31 @@ export function hashTokenTransactionV1(
337
463
  versionHashObj.update(versionBytes);
338
464
  allHashes.push(versionHashObj.digest());
339
465
 
340
- // Hash token inputs if a transfer
466
+ // Hash transaction type
467
+ const typeHashObj = sha256.create();
468
+ const typeBytes = new Uint8Array(4);
469
+ let transactionType = 0;
470
+
471
+ if (tokenTransaction.tokenInputs?.$case === "mintInput") {
472
+ transactionType = TokenTransactionType.TOKEN_TRANSACTION_TYPE_MINT;
473
+ } else if (tokenTransaction.tokenInputs?.$case === "transferInput") {
474
+ transactionType = TokenTransactionType.TOKEN_TRANSACTION_TYPE_TRANSFER;
475
+ } else if (tokenTransaction.tokenInputs?.$case === "createInput") {
476
+ transactionType = TokenTransactionType.TOKEN_TRANSACTION_TYPE_CREATE;
477
+ } else {
478
+ throw new ValidationError(
479
+ "token transaction must have exactly one input type",
480
+ {
481
+ field: "tokenInputs",
482
+ },
483
+ );
484
+ }
485
+
486
+ new DataView(typeBytes.buffer).setUint32(0, transactionType, false);
487
+ typeHashObj.update(typeBytes);
488
+ allHashes.push(typeHashObj.digest());
489
+
490
+ // Hash token inputs based on type
341
491
  if (tokenTransaction.tokenInputs?.$case === "transferInput") {
342
492
  if (!tokenTransaction.tokenInputs.transferInput.outputsToSpend) {
343
493
  throw new ValidationError("outputs to spend cannot be null", {
@@ -353,6 +503,17 @@ export function hashTokenTransactionV1(
353
503
  });
354
504
  }
355
505
 
506
+ // Hash outputs to spend length
507
+ const outputsLenHashObj = sha256.create();
508
+ const outputsLenBytes = new Uint8Array(4);
509
+ new DataView(outputsLenBytes.buffer).setUint32(
510
+ 0,
511
+ tokenTransaction.tokenInputs.transferInput.outputsToSpend.length,
512
+ false,
513
+ );
514
+ outputsLenHashObj.update(outputsLenBytes);
515
+ allHashes.push(outputsLenHashObj.digest());
516
+
356
517
  // Hash outputs to spend
357
518
  for (const [
358
519
  i,
@@ -394,10 +555,7 @@ export function hashTokenTransactionV1(
394
555
 
395
556
  allHashes.push(hashObj.digest());
396
557
  }
397
- }
398
-
399
- // Hash input issuance if a mint
400
- if (tokenTransaction.tokenInputs?.$case === "mintInput") {
558
+ } else if (tokenTransaction.tokenInputs?.$case === "mintInput") {
401
559
  const hashObj = sha256.create();
402
560
 
403
561
  if (tokenTransaction.tokenInputs.mintInput!.issuerPublicKey) {
@@ -412,36 +570,135 @@ export function hashTokenTransactionV1(
412
570
  });
413
571
  }
414
572
  hashObj.update(issuerPubKey);
573
+ allHashes.push(hashObj.digest());
415
574
 
416
- if (
417
- tokenTransaction.tokenInputs.mintInput!.issuerProvidedTimestamp != 0
418
- ) {
419
- const timestampBytes = new Uint8Array(8);
420
- new DataView(timestampBytes.buffer).setBigUint64(
421
- 0,
422
- BigInt(
423
- tokenTransaction.tokenInputs.mintInput!.issuerProvidedTimestamp,
424
- ),
425
- true, // true for little-endian to match Go implementation
575
+ const tokenIdentifierHashObj = sha256.create();
576
+ if (tokenTransaction.tokenInputs.mintInput.tokenIdentifier) {
577
+ tokenIdentifierHashObj.update(
578
+ tokenTransaction.tokenInputs.mintInput.tokenIdentifier,
426
579
  );
427
- hashObj.update(timestampBytes);
580
+ } else {
581
+ tokenIdentifierHashObj.update(new Uint8Array(32));
428
582
  }
429
- allHashes.push(hashObj.digest());
583
+ allHashes.push(tokenIdentifierHashObj.digest());
584
+ }
585
+ } else if (tokenTransaction.tokenInputs?.$case === "createInput") {
586
+ const createInput = tokenTransaction.tokenInputs.createInput!;
587
+
588
+ // Hash issuer public key
589
+ const issuerPubKeyHashObj = sha256.create();
590
+ if (
591
+ !createInput.issuerPublicKey ||
592
+ createInput.issuerPublicKey.length === 0
593
+ ) {
594
+ throw new ValidationError("issuer public key cannot be nil or empty", {
595
+ field: "tokenInputs.createInput.issuerPublicKey",
596
+ });
597
+ }
598
+ issuerPubKeyHashObj.update(createInput.issuerPublicKey);
599
+ allHashes.push(issuerPubKeyHashObj.digest());
600
+
601
+ // Hash token name
602
+ const tokenNameHashObj = sha256.create();
603
+ if (!createInput.tokenName || createInput.tokenName.length === 0) {
604
+ throw new ValidationError("token name cannot be empty", {
605
+ field: "tokenInputs.createInput.tokenName",
606
+ });
607
+ }
608
+ if (createInput.tokenName.length > 20) {
609
+ throw new ValidationError("token name cannot be longer than 20 bytes", {
610
+ field: "tokenInputs.createInput.tokenName",
611
+ value: createInput.tokenName,
612
+ expectedLength: 20,
613
+ actualLength: createInput.tokenName.length,
614
+ });
615
+ }
616
+ const tokenNameEncoder = new TextEncoder();
617
+ tokenNameHashObj.update(tokenNameEncoder.encode(createInput.tokenName));
618
+ allHashes.push(tokenNameHashObj.digest());
619
+
620
+ // Hash token ticker
621
+ const tokenTickerHashObj = sha256.create();
622
+ if (!createInput.tokenTicker || createInput.tokenTicker.length === 0) {
623
+ throw new ValidationError("token ticker cannot be empty", {
624
+ field: "tokenInputs.createInput.tokenTicker",
625
+ });
430
626
  }
627
+ if (createInput.tokenTicker.length > 6) {
628
+ throw new ValidationError("token ticker cannot be longer than 6 bytes", {
629
+ field: "tokenInputs.createInput.tokenTicker",
630
+ value: createInput.tokenTicker,
631
+ expectedLength: 6,
632
+ actualLength: createInput.tokenTicker.length,
633
+ });
634
+ }
635
+ const tokenTickerEncoder = new TextEncoder();
636
+ tokenTickerHashObj.update(
637
+ tokenTickerEncoder.encode(createInput.tokenTicker),
638
+ );
639
+ allHashes.push(tokenTickerHashObj.digest());
640
+
641
+ // Hash decimals
642
+ const decimalsHashObj = sha256.create();
643
+ const decimalsBytes = new Uint8Array(4);
644
+ new DataView(decimalsBytes.buffer).setUint32(
645
+ 0,
646
+ createInput.decimals,
647
+ false,
648
+ );
649
+ decimalsHashObj.update(decimalsBytes);
650
+ allHashes.push(decimalsHashObj.digest());
651
+
652
+ // Hash max supply (fixed 16 bytes)
653
+ const maxSupplyHashObj = sha256.create();
654
+ if (!createInput.maxSupply) {
655
+ throw new ValidationError("max supply cannot be nil", {
656
+ field: "tokenInputs.createInput.maxSupply",
657
+ });
658
+ }
659
+ if (createInput.maxSupply.length !== 16) {
660
+ throw new ValidationError("max supply must be exactly 16 bytes", {
661
+ field: "tokenInputs.createInput.maxSupply",
662
+ value: createInput.maxSupply,
663
+ expectedLength: 16,
664
+ actualLength: createInput.maxSupply.length,
665
+ });
666
+ }
667
+ maxSupplyHashObj.update(createInput.maxSupply);
668
+ allHashes.push(maxSupplyHashObj.digest());
669
+
670
+ // Hash is freezable
671
+ const isFreezableHashObj = sha256.create();
672
+ isFreezableHashObj.update(
673
+ new Uint8Array([createInput.isFreezable ? 1 : 0]),
674
+ );
675
+ allHashes.push(isFreezableHashObj.digest());
676
+
677
+ // Hash creation entity public key (only for final hash)
678
+ const creationEntityHashObj = sha256.create();
679
+ if (!partialHash && createInput.creationEntityPublicKey) {
680
+ creationEntityHashObj.update(createInput.creationEntityPublicKey);
681
+ }
682
+ allHashes.push(creationEntityHashObj.digest());
431
683
  }
432
684
 
433
- // Hash token outputs
685
+ // Hash token outputs (length + contents)
434
686
  if (!tokenTransaction.tokenOutputs) {
435
687
  throw new ValidationError("token outputs cannot be null", {
436
688
  field: "tokenOutputs",
437
689
  });
438
690
  }
439
691
 
440
- if (tokenTransaction.tokenOutputs.length === 0) {
441
- throw new ValidationError("token outputs cannot be empty", {
442
- field: "tokenOutputs",
443
- });
444
- }
692
+ // Hash outputs length
693
+ const outputsLenHashObj = sha256.create();
694
+ const outputsLenBytes = new Uint8Array(4);
695
+ new DataView(outputsLenBytes.buffer).setUint32(
696
+ 0,
697
+ tokenTransaction.tokenOutputs.length,
698
+ false,
699
+ );
700
+ outputsLenHashObj.update(outputsLenBytes);
701
+ allHashes.push(outputsLenHashObj.digest());
445
702
 
446
703
  for (const [i, output] of tokenTransaction.tokenOutputs.entries()) {
447
704
  if (!output) {
@@ -508,18 +765,20 @@ export function hashTokenTransactionV1(
508
765
  hashObj.update(locktimeBytes);
509
766
  }
510
767
 
511
- if (output.tokenPublicKey) {
512
- if (output.tokenPublicKey.length === 0) {
513
- throw new ValidationError(
514
- `token public key at index ${i} cannot be empty`,
515
- {
516
- field: `tokenOutputs[${i}].tokenPublicKey`,
517
- index: i,
518
- },
519
- );
520
- }
768
+ // Hash token public key (33 bytes if present, otherwise 33 zero bytes)
769
+ if (!output.tokenPublicKey || output.tokenPublicKey.length === 0) {
770
+ hashObj.update(new Uint8Array(33));
771
+ } else {
521
772
  hashObj.update(output.tokenPublicKey);
522
773
  }
774
+
775
+ // Hash token identifier (32 bytes if present, otherwise 32 zero bytes)
776
+ if (!output.tokenIdentifier || output.tokenIdentifier.length === 0) {
777
+ hashObj.update(new Uint8Array(32));
778
+ } else {
779
+ hashObj.update(output.tokenIdentifier);
780
+ }
781
+
523
782
  if (output.tokenAmount) {
524
783
  if (output.tokenAmount.length === 0) {
525
784
  throw new ValidationError(
@@ -566,6 +825,17 @@ export function hashTokenTransactionV1(
566
825
  return a.length - b.length;
567
826
  });
568
827
 
828
+ // Hash spark operator identity public keys length
829
+ const operatorLenHashObj = sha256.create();
830
+ const operatorLenBytes = new Uint8Array(4);
831
+ new DataView(operatorLenBytes.buffer).setUint32(
832
+ 0,
833
+ sortedPubKeys.length,
834
+ false,
835
+ );
836
+ operatorLenHashObj.update(operatorLenBytes);
837
+ allHashes.push(operatorLenHashObj.digest());
838
+
569
839
  // Hash spark operator identity public keys
570
840
  for (const [i, pubKey] of sortedPubKeys.entries()) {
571
841
  if (!pubKey) {
@@ -606,9 +876,15 @@ export function hashTokenTransactionV1(
606
876
  const clientTimestampHashObj = sha256.create();
607
877
  const clientCreatedTs: Date | undefined = (tokenTransaction as any)
608
878
  .clientCreatedTimestamp;
609
- const clientUnixTime = clientCreatedTs
610
- ? Math.floor(clientCreatedTs.getTime() / 1000)
611
- : 0;
879
+ if (!clientCreatedTs) {
880
+ throw new ValidationError(
881
+ "client created timestamp cannot be null for V1 token transactions",
882
+ {
883
+ field: "clientCreatedTimestamp",
884
+ },
885
+ );
886
+ }
887
+ const clientUnixTime = clientCreatedTs.getTime();
612
888
  const clientTimestampBytes = new Uint8Array(8);
613
889
  new DataView(clientTimestampBytes.buffer).setBigUint64(
614
890
  0,
@@ -0,0 +1,88 @@
1
+ import { bech32m } from "@scure/base";
2
+
3
+ import { NetworkType } from "../utils/network.js";
4
+ import { ValidationError } from "../errors/index.js";
5
+
6
+ const HumanReadableTokenIdentifierNetworkPrefix: Record<NetworkType, string> = {
7
+ MAINNET: "btk",
8
+ REGTEST: "btkrt",
9
+ TESTNET: "btkt",
10
+ SIGNET: "btks",
11
+ LOCAL: "btkl",
12
+ } as const;
13
+
14
+ export type HumanReadableTokenIdentifier =
15
+ | `btk1${string}`
16
+ | `btkrt1${string}`
17
+ | `btkt1${string}`
18
+ | `btks1${string}`
19
+ | `btkl1${string}`;
20
+
21
+ export interface HumanReadableTokenIdentifierData {
22
+ tokenIdentifier: Uint8Array;
23
+ network: NetworkType;
24
+ }
25
+
26
+ export function encodeHumanReadableTokenIdentifier(
27
+ payload: HumanReadableTokenIdentifierData,
28
+ ): HumanReadableTokenIdentifier {
29
+ try {
30
+ const words = bech32m.toWords(payload.tokenIdentifier);
31
+ return bech32m.encode(
32
+ HumanReadableTokenIdentifierNetworkPrefix[payload.network],
33
+ words,
34
+ 500,
35
+ ) as HumanReadableTokenIdentifier;
36
+ } catch (error) {
37
+ throw new ValidationError(
38
+ "Failed to encode human readable token identifier",
39
+ {
40
+ field: "tokenIdentifier",
41
+ value: payload.tokenIdentifier,
42
+ },
43
+ error as Error,
44
+ );
45
+ }
46
+ }
47
+
48
+ export function decodeHumanReadableTokenIdentifier(
49
+ humanReadableTokenIdentifier: HumanReadableTokenIdentifier,
50
+ network: NetworkType,
51
+ ): HumanReadableTokenIdentifierData {
52
+ try {
53
+ const decoded = bech32m.decode(
54
+ humanReadableTokenIdentifier as HumanReadableTokenIdentifier,
55
+ 500,
56
+ );
57
+
58
+ if (decoded.prefix !== HumanReadableTokenIdentifierNetworkPrefix[network]) {
59
+ throw new ValidationError(
60
+ "Invalid human readable token identifier prefix",
61
+ {
62
+ field: "humanReadableTokenIdentifier",
63
+ value: humanReadableTokenIdentifier,
64
+ expected: `prefix='${HumanReadableTokenIdentifierNetworkPrefix[network]}'`,
65
+ },
66
+ );
67
+ }
68
+
69
+ const tokenIdentifier = bech32m.fromWords(decoded.words);
70
+
71
+ return {
72
+ tokenIdentifier,
73
+ network,
74
+ };
75
+ } catch (error) {
76
+ if (error instanceof ValidationError) {
77
+ throw error;
78
+ }
79
+ throw new ValidationError(
80
+ "Failed to decode human readable token identifier",
81
+ {
82
+ field: "humanReadableTokenIdentifier",
83
+ value: humanReadableTokenIdentifier,
84
+ },
85
+ error as Error,
86
+ );
87
+ }
88
+ }