diff --git a/package-lock.json b/package-lock.json index 96c74793..7a4489a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "13.13.1", + "version": "13.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "13.13.1", + "version": "13.14.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index 0bc0b4cb..7782f9de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "13.13.1", + "version": "13.14.0", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/tokens.spec.ts b/src/tokens.spec.ts index 9a415815..e9a29ef5 100644 --- a/src/tokens.spec.ts +++ b/src/tokens.spec.ts @@ -17,9 +17,17 @@ describe("test tokens and token computer", async () => { let nonce = tokenComputer.extractNonceFromExtendedIdentifier(extendedIdentifier); assert.equal(nonce, 10); + const extendedIdentifierWithPrefix = "test-TEST-123456-0a"; + nonce = tokenComputer.extractNonceFromExtendedIdentifier(extendedIdentifierWithPrefix); + assert.equal(nonce, 10); + const fungibleTokenIdentifier = "FNG-123456"; nonce = tokenComputer.extractNonceFromExtendedIdentifier(fungibleTokenIdentifier); assert.equal(nonce, 0); + + const fungibleTokenIdentifierWithPrefix = "fun-FNG-123456"; + nonce = tokenComputer.extractNonceFromExtendedIdentifier(fungibleTokenIdentifierWithPrefix); + assert.equal(nonce, 0); }); it("should extract identifier from extended identifier", async () => { @@ -27,10 +35,26 @@ describe("test tokens and token computer", async () => { let identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(extendedIdentifier); assert.equal(identifier, "TEST-123456"); + const extendedIdentifierWithPrefix = "t0-TEST-123456-0a"; + identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(extendedIdentifierWithPrefix); + assert.equal(identifier, "t0-TEST-123456"); + + const extendedIdentifierWithPrefixWithoutNonce = "t0-TEST-123456"; + identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(extendedIdentifierWithPrefixWithoutNonce); + assert.equal(identifier, "t0-TEST-123456"); + const fungibleTokenIdentifier = "FNG-123456"; identifier = tokenComputer.extractIdentifierFromExtendedIdentifier(fungibleTokenIdentifier); assert.equal(identifier, "FNG-123456"); }); + + it("should fail if prefix longer than expected", async () => { + const nftIdentifier = "prefix-TEST-123456"; + assert.throw( + () => tokenComputer.extractIdentifierFromExtendedIdentifier(nftIdentifier), + "The identifier is not valid. The prefix does not have the right length", + ); + }); }); describe("test token transfer (legacy)", () => { diff --git a/src/tokens.ts b/src/tokens.ts index 62b22a04..b01c5edf 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1,6 +1,6 @@ import BigNumber from "bignumber.js"; -import { ErrInvalidArgument, ErrInvalidTokenIdentifier } from "./errors"; import { EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER } from "./constants"; +import { ErrInvalidArgument, ErrInvalidTokenIdentifier } from "./errors"; // Legacy constants: const EGLDTokenIdentifier = "EGLD"; @@ -226,6 +226,7 @@ export class TokenTransfer { } export class TokenComputer { + TOKEN_RANDOM_SEQUENCE_LENGTH = 6; constructor() {} isFungible(token: Token): boolean { @@ -235,33 +236,55 @@ export class TokenComputer { extractNonceFromExtendedIdentifier(identifier: string): number { const parts = identifier.split("-"); - this.checkIfExtendedIdentifierWasProvided(parts); - this.checkLengthOfRandomSequence(parts[1]); + const { prefix, ticker, randomSequence } = this.splitIdentifierIntoComponents(parts); + this.validateExtendedIdentifier(prefix, ticker, randomSequence, parts); - // in case the identifier of a fungible token is provided - if (parts.length == 2) { + // If identifier is for a fungible token (2 parts or 3 with prefix), return 0 + if (parts.length === 2 || (prefix && parts.length === 3)) { return 0; } - const hexNonce = Buffer.from(parts[2], "hex"); - return decodeUnsignedNumber(hexNonce); + // Otherwise, decode the last part as an unsigned number + const hexNonce = parts[parts.length - 1]; + return decodeUnsignedNumber(Buffer.from(hexNonce, "hex")); } extractIdentifierFromExtendedIdentifier(identifier: string): string { const parts = identifier.split("-"); + const { prefix, ticker, randomSequence } = this.splitIdentifierIntoComponents(parts); + + this.validateExtendedIdentifier(prefix, ticker, randomSequence, parts); + if (prefix) { + this.checkLengthOfPrefix(prefix); + return prefix + "-" + ticker + "-" + randomSequence; + } + return ticker + "-" + randomSequence; + } + + private validateExtendedIdentifier( + prefix: string | null, + ticker: string, + randomSequence: string, + parts: string[], + ): void { + this.checkIfExtendedIdentifierWasProvided(prefix, parts); + this.ensureTokenTickerValidity(ticker); + this.checkLengthOfRandomSequence(randomSequence); + } - this.checkIfExtendedIdentifierWasProvided(parts); - this.ensureTokenTickerValidity(parts[0]); - this.checkLengthOfRandomSequence(parts[1]); + private splitIdentifierIntoComponents(parts: string[]): { prefix: any; ticker: any; randomSequence: any } { + if (this.isLowercaseAlphanumeric(parts[0])) { + return { prefix: parts[0], ticker: parts[1], randomSequence: parts[2] }; + } - return parts[0] + "-" + parts[1]; + return { prefix: null, ticker: parts[0], randomSequence: parts[1] }; } - private checkIfExtendedIdentifierWasProvided(tokenParts: string[]): void { + private checkIfExtendedIdentifierWasProvided(prefix: string | null, tokenParts: string[]): void { // this is for the identifiers of fungible tokens const MIN_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED = 2; // this is for the identifiers of nft, sft and meta-esdt - const MAX_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED = 3; + const MAX_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED = prefix ? 4 : 3; if ( tokenParts.length < MIN_EXTENDED_IDENTIFIER_LENGTH_IF_SPLITTED || @@ -271,16 +294,28 @@ export class TokenComputer { } } - private checkLengthOfRandomSequence(randomSequence: string): void { - const TOKEN_RANDOM_SEQUENCE_LENGTH = 6; + private isLowercaseAlphanumeric(str: string): boolean { + return /^[a-z0-9]+$/.test(str); + } - if (randomSequence.length !== TOKEN_RANDOM_SEQUENCE_LENGTH) { + private checkLengthOfRandomSequence(randomSequence: string): void { + if (randomSequence.length !== this.TOKEN_RANDOM_SEQUENCE_LENGTH) { throw new ErrInvalidTokenIdentifier( "The identifier is not valid. The random sequence does not have the right length", ); } } + private checkLengthOfPrefix(prefix: string): void { + const MAX_TOKEN_PREFIX_LENGTH = 4; + const MIN_TOKEN_PREFIX_LENGTH = 1; + if (prefix.length < MIN_TOKEN_PREFIX_LENGTH || prefix.length > MAX_TOKEN_PREFIX_LENGTH) { + throw new ErrInvalidTokenIdentifier( + "The identifier is not valid. The prefix does not have the right length", + ); + } + } + private ensureTokenTickerValidity(ticker: string) { const MIN_TICKER_LENGTH = 3; const MAX_TICKER_LENGTH = 10; diff --git a/src/transactionsFactories/transferTransactionsFactory.spec.ts b/src/transactionsFactories/transferTransactionsFactory.spec.ts index 96b55bff..6a7581c3 100644 --- a/src/transactionsFactories/transferTransactionsFactory.spec.ts +++ b/src/transactionsFactories/transferTransactionsFactory.spec.ts @@ -97,6 +97,26 @@ describe("test transfer transactions factory", function () { ); }); + it("should create 'Transaction' for nft transfer with prefix", async () => { + const nft = new Token({ identifier: "t0-NFT-123456", nonce: 10n }); + const transfer = new TokenTransfer({ token: nft, amount: 1n }); + + const transaction = transferFactory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [transfer], + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.value.valueOf(), 0n); + assert.equal(transaction.gasLimit.valueOf(), 1219500n); + assert.deepEqual( + transaction.data.toString(), + "ESDTNFTTransfer@74302d4e46542d313233343536@0a@01@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + ); + }); + it("should create 'Transaction' for multiple nft transfers", async () => { const firstNft = new Token({ identifier: "NFT-123456", nonce: 10n }); const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1n }); @@ -128,6 +148,37 @@ describe("test transfer transactions factory", function () { assert.deepEqual(transaction, secondTransaction); }); + it("should create 'Transaction' for multiple nft transfers with prefix", async () => { + const firstNft = new Token({ identifier: "t0-NFT-123456", nonce: 10n }); + const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1n }); + + const secondNft = new Token({ identifier: "t0-TEST-987654", nonce: 1n }); + const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); + + const transaction = transferFactory.createTransactionForESDTTokenTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.value.valueOf(), 0n); + assert.equal(transaction.gasLimit.valueOf(), 1484000n); + assert.deepEqual( + transaction.data.toString(), + "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@74302d4e46542d313233343536@0a@01@74302d544553542d393837363534@01@01", + ); + + const secondTransaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.deepEqual(transaction, secondTransaction); + }); + it("should fail to create transaction for token transfers", async () => { assert.throws(() => { const nft = new Token({ identifier: "NFT-123456", nonce: 10n }); @@ -207,6 +258,30 @@ describe("test transfer transactions factory", function () { ); }); + it("should create transaction for token transfers with prefix", async () => { + const firstNft = new Token({ identifier: "t0-NFT-123456", nonce: 10n }); + const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1n }); + + const secondNft = new Token({ identifier: "t0-TEST-987654", nonce: 1n }); + const secondTransfer = new TokenTransfer({ token: secondNft, amount: 1n }); + + const transaction = transferFactory.createTransactionForTransfer({ + sender: alice, + receiver: bob, + nativeAmount: 1000000000000000000n, + tokenTransfers: [firstTransfer, secondTransfer], + }); + + assert.equal(transaction.sender, alice.toBech32()); + assert.equal(transaction.receiver, alice.toBech32()); + assert.equal(transaction.value.valueOf(), 0n); + assert.equal(transaction.gasLimit.valueOf(), 1745500n); + assert.deepEqual( + transaction.data.toString(), + "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@03@74302d4e46542d313233343536@0a@01@74302d544553542d393837363534@01@01@45474c442d303030303030@@0de0b6b3a7640000", + ); + }); + it("should create multi transfer for egld", async () => { const firstNft = new Token({ identifier: EGLD_IDENTIFIER_FOR_MULTI_ESDTNFT_TRANSFER }); const firstTransfer = new TokenTransfer({ token: firstNft, amount: 1000000000000000000n });