@buildonspark/issuer-sdk 0.0.78 β 0.0.80
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 +189 -24
- package/dist/index.d.cts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +185 -16
- package/dist/proto/spark.js +1 -1
- package/package.json +4 -4
- package/src/issuer-wallet/issuer-spark-wallet.ts +103 -10
- package/src/services/token-transactions.ts +36 -4
- package/src/tests/integration/spark.test.ts +64 -18
- package/src/tests/stress/transfers.test.ts +13 -6
- package/src/tests/token-create.test.ts +85 -0
- package/src/utils/create-validation.ts +92 -0
|
@@ -139,9 +139,14 @@ describe.each(TEST_CONFIGS)(
|
|
|
139
139
|
const tokenAmount: bigint = 1000n;
|
|
140
140
|
|
|
141
141
|
await sharedIssuerWallet.mintTokens(tokenAmount);
|
|
142
|
+
|
|
143
|
+
const sharedIssuerBalance =
|
|
144
|
+
await sharedIssuerWallet.getIssuerTokenBalance();
|
|
145
|
+
expect(sharedIssuerBalance).toBeDefined();
|
|
146
|
+
expect(sharedIssuerBalance.tokenIdentifier).toBeDefined();
|
|
142
147
|
await sharedIssuerWallet.transferTokens({
|
|
143
148
|
tokenAmount,
|
|
144
|
-
|
|
149
|
+
tokenIdentifier: sharedIssuerBalance.tokenIdentifier!,
|
|
145
150
|
receiverSparkAddress: await sharedUserWallet.getSparkAddress(),
|
|
146
151
|
});
|
|
147
152
|
|
|
@@ -308,24 +313,28 @@ describe.each(TEST_CONFIGS)(
|
|
|
308
313
|
});
|
|
309
314
|
|
|
310
315
|
await sharedIssuerWallet.mintTokens(tokenAmount);
|
|
311
|
-
const
|
|
312
|
-
await sharedIssuerWallet.getIssuerTokenBalance()
|
|
313
|
-
).
|
|
316
|
+
const sharedIssuerBalance =
|
|
317
|
+
await sharedIssuerWallet.getIssuerTokenBalance();
|
|
318
|
+
expect(sharedIssuerBalance).toBeDefined();
|
|
319
|
+
expect(sharedIssuerBalance.tokenIdentifier).toBeDefined();
|
|
320
|
+
|
|
321
|
+
const tokenIdentifier = sharedIssuerBalance.tokenIdentifier!;
|
|
322
|
+
const sourceBalanceBefore = sharedIssuerBalance.balance;
|
|
314
323
|
|
|
315
324
|
await sharedIssuerWallet.batchTransferTokens([
|
|
316
325
|
{
|
|
317
326
|
tokenAmount: tokenAmount / 3n,
|
|
318
|
-
|
|
327
|
+
tokenIdentifier,
|
|
319
328
|
receiverSparkAddress: await destinationWallet.getSparkAddress(),
|
|
320
329
|
},
|
|
321
330
|
{
|
|
322
331
|
tokenAmount: tokenAmount / 3n,
|
|
323
|
-
|
|
332
|
+
tokenIdentifier,
|
|
324
333
|
receiverSparkAddress: await destinationWallet2.getSparkAddress(),
|
|
325
334
|
},
|
|
326
335
|
{
|
|
327
336
|
tokenAmount: tokenAmount / 3n,
|
|
328
|
-
|
|
337
|
+
tokenIdentifier,
|
|
329
338
|
receiverSparkAddress: await destinationWallet3.getSparkAddress(),
|
|
330
339
|
},
|
|
331
340
|
]);
|
|
@@ -461,9 +470,14 @@ describe.each(TEST_CONFIGS)(
|
|
|
461
470
|
await issuerWallet.mintTokens(tokenAmount);
|
|
462
471
|
|
|
463
472
|
// Check issuer balance after minting
|
|
464
|
-
const
|
|
465
|
-
await issuerWallet.getIssuerTokenBalance()
|
|
466
|
-
).
|
|
473
|
+
const issuerBalanceObjAfterMint =
|
|
474
|
+
await issuerWallet.getIssuerTokenBalance();
|
|
475
|
+
expect(issuerBalanceObjAfterMint).toBeDefined();
|
|
476
|
+
expect(issuerBalanceObjAfterMint.tokenIdentifier).toBeDefined();
|
|
477
|
+
|
|
478
|
+
const issuerBalanceAfterMint = issuerBalanceObjAfterMint.balance;
|
|
479
|
+
const tokenIdentifier = issuerBalanceObjAfterMint.tokenIdentifier!;
|
|
480
|
+
|
|
467
481
|
expect(issuerBalanceAfterMint).toEqual(tokenAmount);
|
|
468
482
|
|
|
469
483
|
const { wallet: userWallet } = await SparkWalletTesting.initialize({
|
|
@@ -473,7 +487,7 @@ describe.each(TEST_CONFIGS)(
|
|
|
473
487
|
|
|
474
488
|
await issuerWallet.transferTokens({
|
|
475
489
|
tokenAmount,
|
|
476
|
-
|
|
490
|
+
tokenIdentifier,
|
|
477
491
|
receiverSparkAddress: userWalletPublicKey,
|
|
478
492
|
});
|
|
479
493
|
const issuerBalanceAfterTransfer = (
|
|
@@ -533,17 +547,18 @@ describe.each(TEST_CONFIGS)(
|
|
|
533
547
|
.balance;
|
|
534
548
|
|
|
535
549
|
await sharedIssuerWallet.mintTokens(tokenAmount);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
550
|
+
const issuerBalanceObjAfterMint =
|
|
551
|
+
await sharedIssuerWallet.getIssuerTokenBalance();
|
|
552
|
+
expect(issuerBalanceObjAfterMint).toBeDefined();
|
|
553
|
+
const issuerBalanceAfterMint = issuerBalanceObjAfterMint.balance;
|
|
540
554
|
expect(issuerBalanceAfterMint).toEqual(initialBalance + tokenAmount);
|
|
541
|
-
|
|
555
|
+
expect(issuerBalanceObjAfterMint.tokenIdentifier).toBeDefined();
|
|
556
|
+
const tokenIdentifier = issuerBalanceObjAfterMint.tokenIdentifier!;
|
|
542
557
|
const userWalletPublicKey = await userWallet.getSparkAddress();
|
|
543
558
|
|
|
544
559
|
await sharedIssuerWallet.transferTokens({
|
|
545
560
|
tokenAmount,
|
|
546
|
-
|
|
561
|
+
tokenIdentifier,
|
|
547
562
|
receiverSparkAddress: userWalletPublicKey,
|
|
548
563
|
});
|
|
549
564
|
|
|
@@ -560,7 +575,7 @@ describe.each(TEST_CONFIGS)(
|
|
|
560
575
|
expect(userBalanceAfterTransfer.balance).toEqual(tokenAmount);
|
|
561
576
|
|
|
562
577
|
await userWallet.transferTokens({
|
|
563
|
-
|
|
578
|
+
tokenIdentifier,
|
|
564
579
|
tokenAmount,
|
|
565
580
|
receiverSparkAddress: await sharedIssuerWallet.getSparkAddress(),
|
|
566
581
|
});
|
|
@@ -660,6 +675,37 @@ describe.each(TEST_CONFIGS)(
|
|
|
660
675
|
// expect(transferBackTransaction).toBeDefined();
|
|
661
676
|
// expect(userBurnTransaction).toBeDefined();
|
|
662
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
|
+
);
|
|
663
709
|
},
|
|
664
710
|
);
|
|
665
711
|
|
|
@@ -45,6 +45,10 @@ describe("Stress test for token transfers", () => {
|
|
|
45
45
|
const tokenPublicKey = await issuerWallet.getIdentityPublicKey();
|
|
46
46
|
const userWalletSparkAddress = await userWallet.getSparkAddress();
|
|
47
47
|
const issuerWalletSparkAddress = await issuerWallet.getSparkAddress();
|
|
48
|
+
const issuerBalanceObj = await issuerWallet.getIssuerTokenBalance();
|
|
49
|
+
expect(issuerBalanceObj).toBeDefined();
|
|
50
|
+
expect(issuerBalanceObj.tokenIdentifier).toBeDefined();
|
|
51
|
+
const tokenIdentifier = issuerBalanceObj.tokenIdentifier!;
|
|
48
52
|
|
|
49
53
|
for (let i = 0; i < maxTransactionCycles; i++) {
|
|
50
54
|
if (timeoutReached) {
|
|
@@ -59,7 +63,7 @@ describe("Stress test for token transfers", () => {
|
|
|
59
63
|
try {
|
|
60
64
|
// Transfer tokens from issuer to user
|
|
61
65
|
await issuerWallet.transferTokens({
|
|
62
|
-
|
|
66
|
+
tokenIdentifier,
|
|
63
67
|
tokenAmount: TOKEN_AMOUNT,
|
|
64
68
|
receiverSparkAddress: userWalletSparkAddress,
|
|
65
69
|
});
|
|
@@ -75,7 +79,7 @@ describe("Stress test for token transfers", () => {
|
|
|
75
79
|
|
|
76
80
|
// Transfer tokens from user to issuer
|
|
77
81
|
await userWallet.transferTokens({
|
|
78
|
-
|
|
82
|
+
tokenIdentifier,
|
|
79
83
|
tokenAmount: TOKEN_AMOUNT,
|
|
80
84
|
receiverSparkAddress: issuerWalletSparkAddress,
|
|
81
85
|
});
|
|
@@ -126,20 +130,23 @@ describe("Stress test for token transfers", () => {
|
|
|
126
130
|
});
|
|
127
131
|
|
|
128
132
|
await issuer.wallet.mintTokens(TOKEN_AMOUNT);
|
|
129
|
-
const tokenPublicKey = await issuer.wallet.getIdentityPublicKey();
|
|
130
133
|
const userAddress = await user.wallet.getSparkAddress();
|
|
134
|
+
const issuerBalanceObj = await issuer.wallet.getIssuerTokenBalance();
|
|
135
|
+
expect(issuerBalanceObj).toBeDefined();
|
|
136
|
+
expect(issuerBalanceObj.tokenIdentifier).toBeDefined();
|
|
137
|
+
const tokenIdentifier = issuerBalanceObj.tokenIdentifier!;
|
|
131
138
|
|
|
132
|
-
return { issuer,
|
|
139
|
+
return { issuer, tokenIdentifier, userAddress };
|
|
133
140
|
}),
|
|
134
141
|
);
|
|
135
142
|
|
|
136
143
|
const transactions = walletPairs.map(
|
|
137
|
-
({ issuer,
|
|
144
|
+
({ issuer, tokenIdentifier, userAddress }) =>
|
|
138
145
|
async () => {
|
|
139
146
|
const start_time = Date.now();
|
|
140
147
|
try {
|
|
141
148
|
await issuer.wallet.transferTokens({
|
|
142
|
-
|
|
149
|
+
tokenIdentifier,
|
|
143
150
|
tokenAmount: TOKEN_AMOUNT,
|
|
144
151
|
receiverSparkAddress: userAddress,
|
|
145
152
|
});
|
|
@@ -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 (eΜ)
|
|
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
|
+
}
|