@buildonspark/issuer-sdk 0.0.79 → 0.0.81

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @buildonspark/issuer-sdk
2
2
 
3
+ ## 0.0.81
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @buildonspark/spark-sdk@0.2.2
9
+
10
+ ## 0.0.80
11
+
12
+ ### Patch Changes
13
+
14
+ - tokens changes
15
+ - Bech32mTokenIdentifier prefix change from "btk" -> "btkn"
16
+
17
+ - Updated dependencies
18
+ - @buildonspark/lrc20-sdk@0.0.60
19
+ - @buildonspark/spark-sdk@0.2.1
20
+
3
21
  ## 0.0.79
4
22
 
5
23
  ### Patch Changes
@@ -11,3 +11,7 @@ if (typeof window !== "undefined") {
11
11
  window.globalThis = window;
12
12
  }
13
13
  }
14
+
15
+ export {
16
+ Buffer
17
+ };
package/dist/index.cjs CHANGED
@@ -40,9 +40,9 @@ if (typeof window !== "undefined") {
40
40
 
41
41
  // src/issuer-wallet/issuer-spark-wallet.ts
42
42
  var import_lrc20_sdk = require("@buildonspark/lrc20-sdk");
43
- var import_spark_sdk4 = require("@buildonspark/spark-sdk");
44
- var import_core = require("@lightsparkdev/core");
45
43
  var import_spark_sdk5 = require("@buildonspark/spark-sdk");
44
+ var import_core = require("@lightsparkdev/core");
45
+ var import_spark_sdk6 = require("@buildonspark/spark-sdk");
46
46
  var import_utils4 = require("@noble/curves/abstract/utils");
47
47
 
48
48
  // src/services/freeze.ts
@@ -204,13 +204,105 @@ var IssuerTokenTransactionService = class extends import_spark_sdk3.TokenTransac
204
204
  expiryTime: void 0
205
205
  };
206
206
  }
207
+ async constructCreateTokenTransaction(tokenPublicKey, tokenName, tokenTicker, decimals, maxSupply, isFreezable) {
208
+ return {
209
+ version: 1,
210
+ network: this.config.getNetworkProto(),
211
+ tokenInputs: {
212
+ $case: "createInput",
213
+ createInput: {
214
+ issuerPublicKey: tokenPublicKey,
215
+ tokenName,
216
+ tokenTicker,
217
+ decimals,
218
+ maxSupply: (0, import_utils3.numberToBytesBE)(maxSupply, 16),
219
+ isFreezable
220
+ }
221
+ },
222
+ tokenOutputs: [],
223
+ clientCreatedTimestamp: /* @__PURE__ */ new Date(),
224
+ sparkOperatorIdentityPublicKeys: super.collectOperatorIdentityPublicKeys(),
225
+ expiryTime: void 0
226
+ };
227
+ }
207
228
  };
208
229
 
209
230
  // src/issuer-wallet/issuer-spark-wallet.ts
210
- var import_spark_sdk6 = require("@buildonspark/spark-sdk");
211
231
  var import_spark_sdk7 = require("@buildonspark/spark-sdk");
232
+
233
+ // src/utils/create-validation.ts
234
+ var import_spark_sdk4 = require("@buildonspark/spark-sdk");
235
+ function isNfcNormalized(value) {
236
+ return value.normalize("NFC") === value;
237
+ }
238
+ var MIN_NAME_SIZE = 3;
239
+ var MAX_NAME_SIZE = 20;
240
+ var MIN_SYMBOL_SIZE = 3;
241
+ var MAX_SYMBOL_SIZE = 6;
242
+ var MAX_DECIMALS = 255;
243
+ var MAXIMUM_MAX_SUPPLY = (1n << 128n) - 1n;
244
+ function validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply) {
245
+ if (!isNfcNormalized(tokenName)) {
246
+ throw new import_spark_sdk4.ValidationError("Token name must be NFC-normalised UTF-8", {
247
+ field: "tokenName",
248
+ value: tokenName,
249
+ expected: "NFC normalised string"
250
+ });
251
+ }
252
+ if (!isNfcNormalized(tokenTicker)) {
253
+ throw new import_spark_sdk4.ValidationError("Token ticker must be NFC-normalised UTF-8", {
254
+ field: "tokenTicker",
255
+ value: tokenTicker,
256
+ expected: "NFC normalised string"
257
+ });
258
+ }
259
+ const nameBytes = import_buffer.Buffer.from(tokenName, "utf-8").length;
260
+ if (nameBytes < MIN_NAME_SIZE || nameBytes > MAX_NAME_SIZE) {
261
+ throw new import_spark_sdk4.ValidationError(
262
+ `Token name must be between ${MIN_NAME_SIZE} and ${MAX_NAME_SIZE} bytes`,
263
+ {
264
+ field: "tokenName",
265
+ value: tokenName,
266
+ actualLength: nameBytes,
267
+ expected: `>=${MIN_NAME_SIZE} and <=${MAX_NAME_SIZE}`
268
+ }
269
+ );
270
+ }
271
+ const tickerBytes = import_buffer.Buffer.from(tokenTicker, "utf-8").length;
272
+ if (tickerBytes < MIN_SYMBOL_SIZE || tickerBytes > MAX_SYMBOL_SIZE) {
273
+ throw new import_spark_sdk4.ValidationError(
274
+ `Token ticker must be between ${MIN_SYMBOL_SIZE} and ${MAX_SYMBOL_SIZE} bytes`,
275
+ {
276
+ field: "tokenTicker",
277
+ value: tokenTicker,
278
+ actualLength: tickerBytes,
279
+ expected: `>=${MIN_SYMBOL_SIZE} and <=${MAX_SYMBOL_SIZE}`
280
+ }
281
+ );
282
+ }
283
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {
284
+ throw new import_spark_sdk4.ValidationError(
285
+ `Decimals must be an integer between 0 and ${MAX_DECIMALS}`,
286
+ {
287
+ field: "decimals",
288
+ value: decimals,
289
+ expected: `>=0 and <=${MAX_DECIMALS}`
290
+ }
291
+ );
292
+ }
293
+ if (maxSupply < 0n || maxSupply > MAXIMUM_MAX_SUPPLY) {
294
+ throw new import_spark_sdk4.ValidationError(`maxSupply must be between 0 and 2^128-1`, {
295
+ field: "maxSupply",
296
+ value: maxSupply.toString(),
297
+ expected: `>=0 and <=${MAXIMUM_MAX_SUPPLY.toString()}`
298
+ });
299
+ }
300
+ }
301
+
302
+ // src/issuer-wallet/issuer-spark-wallet.ts
303
+ var import_spark_sdk8 = require("@buildonspark/spark-sdk");
212
304
  var BURN_ADDRESS = "02".repeat(33);
213
- var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.SparkWallet {
305
+ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk5.SparkWallet {
214
306
  issuerTokenTransactionService;
215
307
  tokenFreezeService;
216
308
  tracerId = "issuer-sdk";
@@ -336,7 +428,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
336
428
  issuerPublicKeys: Array.of((0, import_utils4.hexToBytes)(issuerPublicKey))
337
429
  });
338
430
  if (response.tokenMetadata.length === 0) {
339
- throw new import_spark_sdk4.ValidationError(
431
+ throw new import_spark_sdk5.ValidationError(
340
432
  "Token metadata not found - If a token has not yet been announced, please announce. If a token was recently announced, it is being confirmed. Try again in a few seconds.",
341
433
  {
342
434
  field: "tokenMetadata",
@@ -348,7 +440,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
348
440
  );
349
441
  }
350
442
  const metadata = response.tokenMetadata[0];
351
- const tokenIdentifier = (0, import_spark_sdk7.encodeBech32mTokenIdentifier)({
443
+ const tokenIdentifier = (0, import_spark_sdk8.encodeBech32mTokenIdentifier)({
352
444
  tokenIdentifier: metadata.tokenIdentifier,
353
445
  network: this.config.getNetworkType()
354
446
  });
@@ -363,7 +455,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
363
455
  isFreezable: metadata.isFreezable
364
456
  };
365
457
  } catch (error) {
366
- throw new import_spark_sdk4.NetworkError("Failed to fetch token metadata", {
458
+ throw new import_spark_sdk5.NetworkError("Failed to fetch token metadata", {
367
459
  errorCount: 1,
368
460
  errors: error instanceof Error ? error.message : String(error)
369
461
  });
@@ -376,11 +468,47 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
376
468
  */
377
469
  async getIssuerTokenIdentifier() {
378
470
  const tokenMetadata = await this.getIssuerTokenMetadata();
379
- return (0, import_spark_sdk7.encodeBech32mTokenIdentifier)({
471
+ return (0, import_spark_sdk8.encodeBech32mTokenIdentifier)({
380
472
  tokenIdentifier: tokenMetadata.rawTokenIdentifier,
381
473
  network: this.config.getNetworkType()
382
474
  });
383
475
  }
476
+ /**
477
+ * Create a new token on Spark.
478
+ *
479
+ * @param params - Object containing token creation parameters.
480
+ * @param params.tokenName - The name of the token.
481
+ * @param params.tokenTicker - The ticker symbol for the token.
482
+ * @param params.decimals - The number of decimal places for the token.
483
+ * @param params.isFreezable - Whether the token can be frozen.
484
+ * @param [params.maxSupply=0n] - (Optional) The maximum supply of the token. Defaults to <code>0n</code>.
485
+ *
486
+ * @returns The transaction ID of the announcement.
487
+ *
488
+ * @throws {ValidationError} If `decimals` is not a safe integer or other validation fails.
489
+ * @throws {NetworkError} If the announcement transaction cannot be broadcast.
490
+ */
491
+ async createToken({
492
+ tokenName,
493
+ tokenTicker,
494
+ decimals,
495
+ isFreezable,
496
+ maxSupply = 0n
497
+ }) {
498
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
499
+ const issuerPublicKey = await super.getIdentityPublicKey();
500
+ const tokenTransaction = await this.issuerTokenTransactionService.constructCreateTokenTransaction(
501
+ (0, import_utils4.hexToBytes)(issuerPublicKey),
502
+ tokenName,
503
+ tokenTicker,
504
+ decimals,
505
+ maxSupply,
506
+ isFreezable
507
+ );
508
+ return await this.issuerTokenTransactionService.broadcastTokenTransaction(
509
+ tokenTransaction
510
+ );
511
+ }
384
512
  /**
385
513
  * Mints new tokens
386
514
  * @param tokenAmount - The amount of tokens to mint
@@ -415,13 +543,13 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
415
543
  * @returns The transaction ID of the burn operation
416
544
  */
417
545
  async burnTokens(tokenAmount, selectedOutputs) {
418
- const burnAddress = (0, import_spark_sdk5.encodeSparkAddress)({
546
+ const burnAddress = (0, import_spark_sdk6.encodeSparkAddress)({
419
547
  identityPublicKey: BURN_ADDRESS,
420
548
  network: this.config.getNetworkType()
421
549
  });
422
550
  const issuerTokenIdentifier = await this.getIssuerTokenIdentifier();
423
551
  if (issuerTokenIdentifier === null) {
424
- throw new import_spark_sdk4.ValidationError("Issuer token identifier not found");
552
+ throw new import_spark_sdk5.ValidationError("Issuer token identifier not found");
425
553
  }
426
554
  return await this.transferTokens({
427
555
  tokenIdentifier: issuerTokenIdentifier,
@@ -438,7 +566,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
438
566
  async freezeTokens(sparkAddress) {
439
567
  await this.syncTokenOutputs();
440
568
  const tokenPublicKey = await super.getIdentityPublicKey();
441
- const decodedOwnerPubkey = (0, import_spark_sdk5.decodeSparkAddress)(
569
+ const decodedOwnerPubkey = (0, import_spark_sdk6.decodeSparkAddress)(
442
570
  sparkAddress,
443
571
  this.config.getNetworkType()
444
572
  );
@@ -460,7 +588,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
460
588
  async unfreezeTokens(sparkAddress) {
461
589
  await this.syncTokenOutputs();
462
590
  const tokenPublicKey = await super.getIdentityPublicKey();
463
- const decodedOwnerPubkey = (0, import_spark_sdk5.decodeSparkAddress)(
591
+ const decodedOwnerPubkey = (0, import_spark_sdk6.decodeSparkAddress)(
464
592
  sparkAddress,
465
593
  this.config.getNetworkType()
466
594
  );
@@ -479,7 +607,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
479
607
  * @throws {NotImplementedError} This feature is not yet supported
480
608
  */
481
609
  async getIssuerTokenDistribution() {
482
- throw new import_spark_sdk6.NotImplementedError("Token distribution is not yet supported");
610
+ throw new import_spark_sdk7.NotImplementedError("Token distribution is not yet supported");
483
611
  }
484
612
  /**
485
613
  * Announces a new token on the L1 (Bitcoin) network.
@@ -494,8 +622,9 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
494
622
  * @throws {NetworkError} If the announcement transaction cannot be broadcast
495
623
  */
496
624
  async announceTokenL1(tokenName, tokenTicker, decimals, maxSupply, isFreezable, feeRateSatsPerVb = 4) {
625
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
497
626
  if (!Number.isSafeInteger(decimals)) {
498
- throw new import_spark_sdk4.ValidationError("Decimals must be less than 2^53", {
627
+ throw new import_spark_sdk5.ValidationError("Decimals must be less than 2^53", {
499
628
  field: "decimals",
500
629
  value: decimals,
501
630
  expected: "smaller or equal to " + Number.MAX_SAFE_INTEGER
@@ -521,7 +650,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends import_spark_sdk4.Spark
521
650
  );
522
651
  return txId;
523
652
  } catch (error) {
524
- throw new import_spark_sdk4.NetworkError(
653
+ throw new import_spark_sdk5.NetworkError(
525
654
  "Failed to broadcast announcement transaction on L1",
526
655
  {
527
656
  operation: "broadcastRawBtcTransaction",
package/dist/index.d.cts CHANGED
@@ -86,6 +86,28 @@ declare class IssuerSparkWallet extends SparkWallet {
86
86
  * @throws {NetworkError} If the token identifier cannot be retrieved
87
87
  */
88
88
  getIssuerTokenIdentifier(): Promise<Bech32mTokenIdentifier | null>;
89
+ /**
90
+ * Create a new token on Spark.
91
+ *
92
+ * @param params - Object containing token creation parameters.
93
+ * @param params.tokenName - The name of the token.
94
+ * @param params.tokenTicker - The ticker symbol for the token.
95
+ * @param params.decimals - The number of decimal places for the token.
96
+ * @param params.isFreezable - Whether the token can be frozen.
97
+ * @param [params.maxSupply=0n] - (Optional) The maximum supply of the token. Defaults to <code>0n</code>.
98
+ *
99
+ * @returns The transaction ID of the announcement.
100
+ *
101
+ * @throws {ValidationError} If `decimals` is not a safe integer or other validation fails.
102
+ * @throws {NetworkError} If the announcement transaction cannot be broadcast.
103
+ */
104
+ createToken({ tokenName, tokenTicker, decimals, isFreezable, maxSupply, }: {
105
+ tokenName: string;
106
+ tokenTicker: string;
107
+ decimals: number;
108
+ isFreezable: boolean;
109
+ maxSupply?: bigint;
110
+ }): Promise<string>;
89
111
  /**
90
112
  * Mints new tokens
91
113
  * @param tokenAmount - The amount of tokens to mint
package/dist/index.d.ts CHANGED
@@ -86,6 +86,28 @@ declare class IssuerSparkWallet extends SparkWallet {
86
86
  * @throws {NetworkError} If the token identifier cannot be retrieved
87
87
  */
88
88
  getIssuerTokenIdentifier(): Promise<Bech32mTokenIdentifier | null>;
89
+ /**
90
+ * Create a new token on Spark.
91
+ *
92
+ * @param params - Object containing token creation parameters.
93
+ * @param params.tokenName - The name of the token.
94
+ * @param params.tokenTicker - The ticker symbol for the token.
95
+ * @param params.decimals - The number of decimal places for the token.
96
+ * @param params.isFreezable - Whether the token can be frozen.
97
+ * @param [params.maxSupply=0n] - (Optional) The maximum supply of the token. Defaults to <code>0n</code>.
98
+ *
99
+ * @returns The transaction ID of the announcement.
100
+ *
101
+ * @throws {ValidationError} If `decimals` is not a safe integer or other validation fails.
102
+ * @throws {NetworkError} If the announcement transaction cannot be broadcast.
103
+ */
104
+ createToken({ tokenName, tokenTicker, decimals, isFreezable, maxSupply, }: {
105
+ tokenName: string;
106
+ tokenTicker: string;
107
+ decimals: number;
108
+ isFreezable: boolean;
109
+ maxSupply?: bigint;
110
+ }): Promise<string>;
89
111
  /**
90
112
  * Mints new tokens
91
113
  * @param tokenAmount - The amount of tokens to mint
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import "./chunk-5MYKP7NN.js";
1
+ import {
2
+ Buffer
3
+ } from "./chunk-7B4B24XF.js";
2
4
 
3
5
  // src/issuer-wallet/issuer-spark-wallet.ts
4
6
  import { TokenPubkey, TokenPubkeyAnnouncement } from "@buildonspark/lrc20-sdk";
5
7
  import {
6
8
  NetworkError as NetworkError2,
7
9
  SparkWallet,
8
- ValidationError as ValidationError2
10
+ ValidationError as ValidationError3
9
11
  } from "@buildonspark/spark-sdk";
10
12
  import { isNode } from "@lightsparkdev/core";
11
13
  import {
@@ -180,10 +182,102 @@ var IssuerTokenTransactionService = class extends TokenTransactionService {
180
182
  expiryTime: void 0
181
183
  };
182
184
  }
185
+ async constructCreateTokenTransaction(tokenPublicKey, tokenName, tokenTicker, decimals, maxSupply, isFreezable) {
186
+ return {
187
+ version: 1,
188
+ network: this.config.getNetworkProto(),
189
+ tokenInputs: {
190
+ $case: "createInput",
191
+ createInput: {
192
+ issuerPublicKey: tokenPublicKey,
193
+ tokenName,
194
+ tokenTicker,
195
+ decimals,
196
+ maxSupply: numberToBytesBE(maxSupply, 16),
197
+ isFreezable
198
+ }
199
+ },
200
+ tokenOutputs: [],
201
+ clientCreatedTimestamp: /* @__PURE__ */ new Date(),
202
+ sparkOperatorIdentityPublicKeys: super.collectOperatorIdentityPublicKeys(),
203
+ expiryTime: void 0
204
+ };
205
+ }
183
206
  };
184
207
 
185
208
  // src/issuer-wallet/issuer-spark-wallet.ts
186
209
  import { NotImplementedError } from "@buildonspark/spark-sdk";
210
+
211
+ // src/utils/create-validation.ts
212
+ import { ValidationError as ValidationError2 } from "@buildonspark/spark-sdk";
213
+ function isNfcNormalized(value) {
214
+ return value.normalize("NFC") === value;
215
+ }
216
+ var MIN_NAME_SIZE = 3;
217
+ var MAX_NAME_SIZE = 20;
218
+ var MIN_SYMBOL_SIZE = 3;
219
+ var MAX_SYMBOL_SIZE = 6;
220
+ var MAX_DECIMALS = 255;
221
+ var MAXIMUM_MAX_SUPPLY = (1n << 128n) - 1n;
222
+ function validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply) {
223
+ if (!isNfcNormalized(tokenName)) {
224
+ throw new ValidationError2("Token name must be NFC-normalised UTF-8", {
225
+ field: "tokenName",
226
+ value: tokenName,
227
+ expected: "NFC normalised string"
228
+ });
229
+ }
230
+ if (!isNfcNormalized(tokenTicker)) {
231
+ throw new ValidationError2("Token ticker must be NFC-normalised UTF-8", {
232
+ field: "tokenTicker",
233
+ value: tokenTicker,
234
+ expected: "NFC normalised string"
235
+ });
236
+ }
237
+ const nameBytes = Buffer.from(tokenName, "utf-8").length;
238
+ if (nameBytes < MIN_NAME_SIZE || nameBytes > MAX_NAME_SIZE) {
239
+ throw new ValidationError2(
240
+ `Token name must be between ${MIN_NAME_SIZE} and ${MAX_NAME_SIZE} bytes`,
241
+ {
242
+ field: "tokenName",
243
+ value: tokenName,
244
+ actualLength: nameBytes,
245
+ expected: `>=${MIN_NAME_SIZE} and <=${MAX_NAME_SIZE}`
246
+ }
247
+ );
248
+ }
249
+ const tickerBytes = Buffer.from(tokenTicker, "utf-8").length;
250
+ if (tickerBytes < MIN_SYMBOL_SIZE || tickerBytes > MAX_SYMBOL_SIZE) {
251
+ throw new ValidationError2(
252
+ `Token ticker must be between ${MIN_SYMBOL_SIZE} and ${MAX_SYMBOL_SIZE} bytes`,
253
+ {
254
+ field: "tokenTicker",
255
+ value: tokenTicker,
256
+ actualLength: tickerBytes,
257
+ expected: `>=${MIN_SYMBOL_SIZE} and <=${MAX_SYMBOL_SIZE}`
258
+ }
259
+ );
260
+ }
261
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {
262
+ throw new ValidationError2(
263
+ `Decimals must be an integer between 0 and ${MAX_DECIMALS}`,
264
+ {
265
+ field: "decimals",
266
+ value: decimals,
267
+ expected: `>=0 and <=${MAX_DECIMALS}`
268
+ }
269
+ );
270
+ }
271
+ if (maxSupply < 0n || maxSupply > MAXIMUM_MAX_SUPPLY) {
272
+ throw new ValidationError2(`maxSupply must be between 0 and 2^128-1`, {
273
+ field: "maxSupply",
274
+ value: maxSupply.toString(),
275
+ expected: `>=0 and <=${MAXIMUM_MAX_SUPPLY.toString()}`
276
+ });
277
+ }
278
+ }
279
+
280
+ // src/issuer-wallet/issuer-spark-wallet.ts
187
281
  import {
188
282
  encodeBech32mTokenIdentifier
189
283
  } from "@buildonspark/spark-sdk";
@@ -314,7 +408,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends SparkWallet {
314
408
  issuerPublicKeys: Array.of(hexToBytes2(issuerPublicKey))
315
409
  });
316
410
  if (response.tokenMetadata.length === 0) {
317
- throw new ValidationError2(
411
+ throw new ValidationError3(
318
412
  "Token metadata not found - If a token has not yet been announced, please announce. If a token was recently announced, it is being confirmed. Try again in a few seconds.",
319
413
  {
320
414
  field: "tokenMetadata",
@@ -359,6 +453,42 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends SparkWallet {
359
453
  network: this.config.getNetworkType()
360
454
  });
361
455
  }
456
+ /**
457
+ * Create a new token on Spark.
458
+ *
459
+ * @param params - Object containing token creation parameters.
460
+ * @param params.tokenName - The name of the token.
461
+ * @param params.tokenTicker - The ticker symbol for the token.
462
+ * @param params.decimals - The number of decimal places for the token.
463
+ * @param params.isFreezable - Whether the token can be frozen.
464
+ * @param [params.maxSupply=0n] - (Optional) The maximum supply of the token. Defaults to <code>0n</code>.
465
+ *
466
+ * @returns The transaction ID of the announcement.
467
+ *
468
+ * @throws {ValidationError} If `decimals` is not a safe integer or other validation fails.
469
+ * @throws {NetworkError} If the announcement transaction cannot be broadcast.
470
+ */
471
+ async createToken({
472
+ tokenName,
473
+ tokenTicker,
474
+ decimals,
475
+ isFreezable,
476
+ maxSupply = 0n
477
+ }) {
478
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
479
+ const issuerPublicKey = await super.getIdentityPublicKey();
480
+ const tokenTransaction = await this.issuerTokenTransactionService.constructCreateTokenTransaction(
481
+ hexToBytes2(issuerPublicKey),
482
+ tokenName,
483
+ tokenTicker,
484
+ decimals,
485
+ maxSupply,
486
+ isFreezable
487
+ );
488
+ return await this.issuerTokenTransactionService.broadcastTokenTransaction(
489
+ tokenTransaction
490
+ );
491
+ }
362
492
  /**
363
493
  * Mints new tokens
364
494
  * @param tokenAmount - The amount of tokens to mint
@@ -399,7 +529,7 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends SparkWallet {
399
529
  });
400
530
  const issuerTokenIdentifier = await this.getIssuerTokenIdentifier();
401
531
  if (issuerTokenIdentifier === null) {
402
- throw new ValidationError2("Issuer token identifier not found");
532
+ throw new ValidationError3("Issuer token identifier not found");
403
533
  }
404
534
  return await this.transferTokens({
405
535
  tokenIdentifier: issuerTokenIdentifier,
@@ -472,8 +602,9 @@ var IssuerSparkWallet = class _IssuerSparkWallet extends SparkWallet {
472
602
  * @throws {NetworkError} If the announcement transaction cannot be broadcast
473
603
  */
474
604
  async announceTokenL1(tokenName, tokenTicker, decimals, maxSupply, isFreezable, feeRateSatsPerVb = 4) {
605
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
475
606
  if (!Number.isSafeInteger(decimals)) {
476
- throw new ValidationError2("Decimals must be less than 2^53", {
607
+ throw new ValidationError3("Decimals must be less than 2^53", {
477
608
  field: "decimals",
478
609
  value: decimals,
479
610
  expected: "smaller or equal to " + Number.MAX_SAFE_INTEGER
@@ -1,4 +1,4 @@
1
- import "../chunk-5MYKP7NN.js";
1
+ import "../chunk-7B4B24XF.js";
2
2
 
3
3
  // src/proto/spark.ts
4
4
  export * from "@buildonspark/spark-sdk/proto/spark";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildonspark/issuer-sdk",
3
- "version": "0.0.79",
3
+ "version": "0.0.81",
4
4
  "description": "Spark Issuer SDK for token issuance",
5
5
  "license": "Apache-2.0",
6
6
  "module": "./dist/index.js",
@@ -47,15 +47,15 @@
47
47
  "package:checks": "yarn depcheck && yarn attw --pack . && echo \"\nPackage checks passed successfully!\"",
48
48
  "postversion": "yarn build",
49
49
  "test-cmd": "node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --detectOpenHandles --forceExit",
50
- "test": "echo \"Error: no tests yet\"",
50
+ "test": "yarn test-cmd src/tests/*.test.ts",
51
51
  "test:integration": "HERMETIC_TEST=true yarn test-cmd src/tests/integration/*.test.ts",
52
52
  "test:stress": "yarn test-cmd src/tests/stress/*.test.ts",
53
53
  "types:watch": "tsc-absolute --watch",
54
54
  "types": "tsc"
55
55
  },
56
56
  "dependencies": {
57
- "@buildonspark/lrc20-sdk": "0.0.59",
58
- "@buildonspark/spark-sdk": "0.2.0",
57
+ "@buildonspark/lrc20-sdk": "0.0.60",
58
+ "@buildonspark/spark-sdk": "0.2.2",
59
59
  "@lightsparkdev/core": "^1.4.2",
60
60
  "@noble/curves": "^1.8.0",
61
61
  "@scure/btc-signer": "^1.5.0",
@@ -3,7 +3,6 @@ import {
3
3
  NetworkError,
4
4
  SparkWallet,
5
5
  SparkWalletProps,
6
- UserTokenMetadata,
7
6
  ValidationError,
8
7
  } from "@buildonspark/spark-sdk";
9
8
  import { isNode } from "@lightsparkdev/core";
@@ -27,6 +26,7 @@ import { IssuerTokenTransactionService } from "../services/token-transactions.js
27
26
  import { TokenDistribution, IssuerTokenMetadata } from "./types.js";
28
27
  import { NotImplementedError } from "@buildonspark/spark-sdk";
29
28
  import { SparkSigner } from "@buildonspark/spark-sdk";
29
+ import { validateTokenParameters } from "../utils/create-validation.js";
30
30
  import {
31
31
  encodeBech32mTokenIdentifier,
32
32
  Bech32mTokenIdentifier,
@@ -230,6 +230,53 @@ export class IssuerSparkWallet extends SparkWallet {
230
230
  });
231
231
  }
232
232
 
233
+ /**
234
+ * Create a new token on Spark.
235
+ *
236
+ * @param params - Object containing token creation parameters.
237
+ * @param params.tokenName - The name of the token.
238
+ * @param params.tokenTicker - The ticker symbol for the token.
239
+ * @param params.decimals - The number of decimal places for the token.
240
+ * @param params.isFreezable - Whether the token can be frozen.
241
+ * @param [params.maxSupply=0n] - (Optional) The maximum supply of the token. Defaults to <code>0n</code>.
242
+ *
243
+ * @returns The transaction ID of the announcement.
244
+ *
245
+ * @throws {ValidationError} If `decimals` is not a safe integer or other validation fails.
246
+ * @throws {NetworkError} If the announcement transaction cannot be broadcast.
247
+ */
248
+ public async createToken({
249
+ tokenName,
250
+ tokenTicker,
251
+ decimals,
252
+ isFreezable,
253
+ maxSupply = 0n,
254
+ }: {
255
+ tokenName: string;
256
+ tokenTicker: string;
257
+ decimals: number;
258
+ isFreezable: boolean;
259
+ maxSupply?: bigint;
260
+ }): Promise<string> {
261
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
262
+
263
+ const issuerPublicKey = await super.getIdentityPublicKey();
264
+
265
+ const tokenTransaction =
266
+ await this.issuerTokenTransactionService.constructCreateTokenTransaction(
267
+ hexToBytes(issuerPublicKey),
268
+ tokenName,
269
+ tokenTicker,
270
+ decimals,
271
+ maxSupply,
272
+ isFreezable,
273
+ );
274
+
275
+ return await this.issuerTokenTransactionService.broadcastTokenTransaction(
276
+ tokenTransaction,
277
+ );
278
+ }
279
+
233
280
  /**
234
281
  * Mints new tokens
235
282
  * @param tokenAmount - The amount of tokens to mint
@@ -373,6 +420,8 @@ export class IssuerSparkWallet extends SparkWallet {
373
420
  isFreezable: boolean,
374
421
  feeRateSatsPerVb: number = 4.0,
375
422
  ): Promise<string> {
423
+ validateTokenParameters(tokenName, tokenTicker, decimals, maxSupply);
424
+
376
425
  if (!Number.isSafeInteger(decimals)) {
377
426
  throw new ValidationError("Decimals must be less than 2^53", {
378
427
  field: "decimals",
@@ -66,4 +66,34 @@ export class IssuerTokenTransactionService extends TokenTransactionService {
66
66
  expiryTime: undefined,
67
67
  };
68
68
  }
69
+
70
+ async constructCreateTokenTransaction(
71
+ tokenPublicKey: Uint8Array,
72
+ tokenName: string,
73
+ tokenTicker: string,
74
+ decimals: number,
75
+ maxSupply: bigint,
76
+ isFreezable: boolean,
77
+ ): Promise<TokenTransaction> {
78
+ return {
79
+ version: 1,
80
+ network: this.config.getNetworkProto(),
81
+ tokenInputs: {
82
+ $case: "createInput",
83
+ createInput: {
84
+ issuerPublicKey: tokenPublicKey,
85
+ tokenName: tokenName,
86
+ tokenTicker: tokenTicker,
87
+ decimals: decimals,
88
+ maxSupply: numberToBytesBE(maxSupply, 16),
89
+ isFreezable: isFreezable,
90
+ },
91
+ },
92
+ tokenOutputs: [],
93
+ clientCreatedTimestamp: new Date(),
94
+ sparkOperatorIdentityPublicKeys:
95
+ super.collectOperatorIdentityPublicKeys(),
96
+ expiryTime: undefined,
97
+ };
98
+ }
69
99
  }
@@ -675,6 +675,37 @@ describe.each(TEST_CONFIGS)(
675
675
  // expect(transferBackTransaction).toBeDefined();
676
676
  // expect(userBurnTransaction).toBeDefined();
677
677
  // });
678
+
679
+ (config.tokenTransactionVersion === "V0" ? it.skip : it)(
680
+ "should create a token using createToken API",
681
+ async () => {
682
+ const { wallet: issuerWallet } =
683
+ await IssuerSparkWalletTesting.initialize({
684
+ options: config,
685
+ });
686
+
687
+ const tokenName = `${name}Creatable`;
688
+ const tokenTicker = "CRT";
689
+ const maxSupply = 5000n;
690
+ const decimals = 0;
691
+ const txId = await issuerWallet.createToken({
692
+ tokenName,
693
+ tokenTicker,
694
+ decimals,
695
+ isFreezable: false,
696
+ maxSupply,
697
+ });
698
+
699
+ expect(typeof txId).toBe("string");
700
+ expect(txId.length).toBeGreaterThan(0);
701
+
702
+ const metadata = await issuerWallet.getIssuerTokenMetadata();
703
+ expect(metadata.tokenName).toEqual(tokenName);
704
+ expect(metadata.tokenTicker).toEqual(tokenTicker);
705
+ expect(metadata.maxSupply).toEqual(maxSupply);
706
+ expect(metadata.decimals).toEqual(decimals);
707
+ },
708
+ );
678
709
  },
679
710
  );
680
711
 
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { validateTokenParameters } from "../utils/create-validation.js";
3
+
4
+ const MAX_SUPPLY_128 = (1n << 128n) - 1n;
5
+
6
+ describe("validateTokenParameters (V1)", () => {
7
+ describe("valid inputs", () => {
8
+ it("accepts minimum & maximum byte length for name", () => {
9
+ expect(() => validateTokenParameters("abc", "AAA", 0, 1n)).not.toThrow();
10
+ expect(() =>
11
+ validateTokenParameters("12345678901234567890", "AAA", 0, 1n),
12
+ ).not.toThrow();
13
+ });
14
+
15
+ it("accepts minimum & maximum byte length for symbol", () => {
16
+ expect(() =>
17
+ validateTokenParameters("Token", "ABC", 0, 1n),
18
+ ).not.toThrow();
19
+ expect(() =>
20
+ validateTokenParameters("Token", "ABCDEF", 0, 1n),
21
+ ).not.toThrow();
22
+ });
23
+
24
+ it("accepts combined length exactly at upper bound", () => {
25
+ // name 17 bytes + symbol 3 bytes = 20 bytes
26
+ expect(() =>
27
+ validateTokenParameters("ABCDEFGHIJKLMNOPQ", "AAA", 0, 1n),
28
+ ).not.toThrow();
29
+ });
30
+
31
+ it("accepts decimals within 0-255 and maxSupply within u128", () => {
32
+ expect(() =>
33
+ validateTokenParameters("Token", "TOK", 255, MAX_SUPPLY_128),
34
+ ).not.toThrow();
35
+ });
36
+
37
+ it("handles multi-byte UTF-8 characters correctly", () => {
38
+ // "🚀" is 4 bytes in UTF-8
39
+ expect(() =>
40
+ validateTokenParameters("Tok🚀n", "TOK", 8, 1000n),
41
+ ).not.toThrow();
42
+ });
43
+ });
44
+
45
+ describe("invalid inputs", () => {
46
+ it("rejects name too short or too long", () => {
47
+ expect(() => validateTokenParameters("ab", "AAA", 0, 1n)).toThrow();
48
+ expect(() =>
49
+ validateTokenParameters("123456789012345678901", "AAA", 0, 1n),
50
+ ).toThrow();
51
+ });
52
+
53
+ it("rejects symbol too short or too long", () => {
54
+ expect(() => validateTokenParameters("Token", "AB", 0, 1n)).toThrow();
55
+ expect(() =>
56
+ validateTokenParameters("Token", "ABCDEFG", 0, 1n),
57
+ ).toThrow();
58
+ });
59
+
60
+ it("rejects decimals outside 0-255 or non-integer", () => {
61
+ // >255
62
+ expect(() => validateTokenParameters("Token", "TOK", 256, 1n)).toThrow();
63
+ // negative
64
+ expect(() => validateTokenParameters("Token", "TOK", -1, 1n)).toThrow();
65
+ // non-integer (should be rejected by safe-integer check)
66
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
67
+ // @ts-ignore intentional wrong type for test
68
+ expect(() => validateTokenParameters("Token", "TOK", 1.5, 1n)).toThrow();
69
+ });
70
+
71
+ it("rejects decimals >= 2^53", () => {
72
+ const hugeDecimal = 2 ** 53;
73
+ expect(() =>
74
+ validateTokenParameters("Token", "TOK", hugeDecimal, 1n),
75
+ ).toThrow();
76
+ });
77
+
78
+ it("rejects maxSupply outside u128 range", () => {
79
+ expect(() => validateTokenParameters("Token", "TOK", 0, -1n)).toThrow();
80
+ expect(() =>
81
+ validateTokenParameters("Token", "TOK", 0, MAX_SUPPLY_128 + 1n),
82
+ ).toThrow();
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,92 @@
1
+ import { ValidationError } from "@buildonspark/spark-sdk";
2
+
3
+ /**
4
+ * Returns true when the input is already in NFC normalisation form.
5
+ * JavaScript strings are UTF-16 encoded, so any JavaScript string is
6
+ * already valid Unicode. However, we still need to ensure canonical
7
+ * equivalence so that, for example, \u00E9 (é) and \u0065\u0301 (é)
8
+ * are treated identically. We do this by comparing the original
9
+ * string to its NFC-normalised representation.
10
+ */
11
+ function isNfcNormalized(value: string): boolean {
12
+ return value.normalize("NFC") === value;
13
+ }
14
+
15
+ const MIN_NAME_SIZE = 3; // bytes
16
+ const MAX_NAME_SIZE = 20; // bytes
17
+ const MIN_SYMBOL_SIZE = 3; // bytes
18
+ const MAX_SYMBOL_SIZE = 6; // bytes
19
+ const MAX_DECIMALS = 255; // fits into single byte
20
+ const MAXIMUM_MAX_SUPPLY = (1n << 128n) - 1n; // fits into 16 bytes (u128)
21
+
22
+ export function validateTokenParameters(
23
+ tokenName: string,
24
+ tokenTicker: string,
25
+ decimals: number,
26
+ maxSupply: bigint,
27
+ ) {
28
+ if (!isNfcNormalized(tokenName)) {
29
+ throw new ValidationError("Token name must be NFC-normalised UTF-8", {
30
+ field: "tokenName",
31
+ value: tokenName,
32
+ expected: "NFC normalised string",
33
+ });
34
+ }
35
+
36
+ if (!isNfcNormalized(tokenTicker)) {
37
+ throw new ValidationError("Token ticker must be NFC-normalised UTF-8", {
38
+ field: "tokenTicker",
39
+ value: tokenTicker,
40
+ expected: "NFC normalised string",
41
+ });
42
+ }
43
+
44
+ const nameBytes = Buffer.from(tokenName, "utf-8").length;
45
+ if (nameBytes < MIN_NAME_SIZE || nameBytes > MAX_NAME_SIZE) {
46
+ throw new ValidationError(
47
+ `Token name must be between ${MIN_NAME_SIZE} and ${MAX_NAME_SIZE} bytes`,
48
+ {
49
+ field: "tokenName",
50
+ value: tokenName,
51
+ actualLength: nameBytes,
52
+ expected: `>=${MIN_NAME_SIZE} and <=${MAX_NAME_SIZE}`,
53
+ },
54
+ );
55
+ }
56
+
57
+ const tickerBytes = Buffer.from(tokenTicker, "utf-8").length;
58
+ if (tickerBytes < MIN_SYMBOL_SIZE || tickerBytes > MAX_SYMBOL_SIZE) {
59
+ throw new ValidationError(
60
+ `Token ticker must be between ${MIN_SYMBOL_SIZE} and ${MAX_SYMBOL_SIZE} bytes`,
61
+ {
62
+ field: "tokenTicker",
63
+ value: tokenTicker,
64
+ actualLength: tickerBytes,
65
+ expected: `>=${MIN_SYMBOL_SIZE} and <=${MAX_SYMBOL_SIZE}`,
66
+ },
67
+ );
68
+ }
69
+
70
+ if (
71
+ !Number.isSafeInteger(decimals) ||
72
+ decimals < 0 ||
73
+ decimals > MAX_DECIMALS
74
+ ) {
75
+ throw new ValidationError(
76
+ `Decimals must be an integer between 0 and ${MAX_DECIMALS}`,
77
+ {
78
+ field: "decimals",
79
+ value: decimals,
80
+ expected: `>=0 and <=${MAX_DECIMALS}`,
81
+ },
82
+ );
83
+ }
84
+
85
+ if (maxSupply < 0n || maxSupply > MAXIMUM_MAX_SUPPLY) {
86
+ throw new ValidationError(`maxSupply must be between 0 and 2^128-1`, {
87
+ field: "maxSupply",
88
+ value: maxSupply.toString(),
89
+ expected: `>=0 and <=${MAXIMUM_MAX_SUPPLY.toString()}`,
90
+ });
91
+ }
92
+ }