@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 +18 -0
- package/dist/{chunk-5MYKP7NN.js → chunk-7B4B24XF.js} +4 -0
- package/dist/index.cjs +144 -15
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +136 -5
- package/dist/proto/spark.js +1 -1
- package/package.json +4 -4
- package/src/issuer-wallet/issuer-spark-wallet.ts +50 -1
- package/src/services/token-transactions.ts +30 -0
- package/src/tests/integration/spark.test.ts +31 -0
- package/src/tests/token-create.test.ts +85 -0
- package/src/utils/create-validation.ts +92 -0
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
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/proto/spark.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildonspark/issuer-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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": "
|
|
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.
|
|
58
|
-
"@buildonspark/spark-sdk": "0.2.
|
|
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
|
+
}
|