From 2663e75466223896876ceffaafc8872cfb5255fe Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Tue, 20 Feb 2024 16:56:15 +0800 Subject: [PATCH 1/4] feat(implemented nft to accomodate sub indices (e.g. AELFIE-10-999)): --- .../AElf.Contracts.MultiToken/TokenContract_Helper.cs | 2 ++ .../AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs | 8 +++++++- .../TokenContract_NFT_Actions.cs | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs index ed210282d0..10ff43cd15 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs @@ -263,6 +263,8 @@ private void CheckSymbolLength(string symbol, SymbolType symbolType) { if (symbolType == SymbolType.Token) Assert(symbol.Length <= TokenContractConstants.SymbolMaxLength, "Invalid token symbol length"); + + // there is a max length of 30 for NFT symbol, limiting the amount of sub NFTs that can be created if (symbolType == SymbolType.Nft || symbolType == SymbolType.NftCollection) Assert(symbol.Length <= TokenContractConstants.NFTSymbolMaxLength, "Invalid NFT symbol length"); } diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs index bddfb444bf..9863331aff 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs @@ -9,7 +9,13 @@ private SymbolType GetCreateInputSymbolType(string symbol) var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); Assert(words[0].Length > 0 && words[0].All(IsValidCreateSymbolChar), "Invalid Symbol input"); if (words.Length == 1) return SymbolType.Token; - Assert(words.Length == 2 && words[1].Length > 0 && words[1].All(IsValidItemIdChar), "Invalid NFT Symbol input"); + Assert(words.Length >= 2, "Invalid NFT Symbol input"); + for (var i = 1; i < words.Length; ++i) + { + var word = words[i]; + Assert(word.Length > 0 && word.All(IsValidItemIdChar), "Invalid NFT Symbol input"); + } + // if we want to allow sub nft collection to be created with a different owner, we can check against words[^1] return words[1] == TokenContractConstants.CollectionSymbolSuffix ? SymbolType.NftCollection : SymbolType.Nft; } diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs index 1926727e68..d8c354915e 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs @@ -91,7 +91,12 @@ private string GetNftCollectionSymbol(string inputSymbol) var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); const int tokenSymbolLength = 1; if (words.Length == tokenSymbolLength) return null; - Assert(words.Length == 2 && words[1].All(IsValidItemIdChar), "Invalid NFT Symbol Input"); + Assert(words.Length >= 2, "Invalid NFT symbol input"); + for (var i = 1; i < words.Length; ++i) + { + var word = words[i]; + Assert(word.Length > 0 && word.All(IsValidItemIdChar), "Invalid NFT Symbol input"); + } return symbol == $"{words[0]}-0" ? null : $"{words[0]}-0"; } From 96636d793effa6f8d44baf027fa53a0eac5c45a6 Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Tue, 20 Feb 2024 17:34:07 +0800 Subject: [PATCH 2/4] feat(sub nfts): implemented such that on a sub nft, there cannot be a 0 index as 0 represents a collection --- .../TokenContract_Helper.cs | 21 ++++---- .../TokenContract_NFTHelper.cs | 53 +++++++++++++++---- .../TokenContract_NFT_Actions.cs | 9 ++-- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs index 10ff43cd15..bc2f6c7db3 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs @@ -14,16 +14,17 @@ namespace AElf.Contracts.MultiToken; public partial class TokenContract { - private static bool IsValidSymbolChar(char character) - { - return (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || - character == TokenContractConstants.NFTSymbolSeparator; - } - private bool IsValidItemIdChar(char character) { return character >= '0' && character <= '9'; } + + // For checking if a sub item id is valid + // e.g. AELFIE-1-1337 is valid but AELFIE-1-0 is invalid as 0 represents a collection and there should not be any collection in a sub item + private bool IsValidSubItemIdChar(char character) + { + return character >= '1' && character <= '9'; + } private bool IsValidCreateSymbolChar(char character) { @@ -40,8 +41,8 @@ private TokenInfo AssertValidToken(string symbol, long amount) private void AssertValidSymbolAndAmount(string symbol, long amount) { - Assert(!string.IsNullOrEmpty(symbol) && symbol.All(IsValidSymbolChar), - "Invalid symbol."); + Assert(!string.IsNullOrEmpty(symbol), "Invalid symbol."); + AssertSymbolIsValid(symbol); Assert(amount > 0, "Invalid amount."); } @@ -184,8 +185,8 @@ private void AssertCrossChainTransaction(Transaction originalTransaction, Addres private void RegisterTokenInfo(TokenInfo tokenInfo) { CheckTokenExists(tokenInfo.Symbol); - Assert(!string.IsNullOrEmpty(tokenInfo.Symbol) && tokenInfo.Symbol.All(IsValidSymbolChar), - "Invalid symbol."); + Assert(!string.IsNullOrEmpty(tokenInfo.Symbol), "Invalid symbol."); + AssertSymbolIsValid(tokenInfo.Symbol); Assert(!string.IsNullOrEmpty(tokenInfo.TokenName), "Token name can neither be null nor empty."); Assert(tokenInfo.TotalSupply > 0, "Invalid total supply."); Assert(tokenInfo.Issuer != null, "Invalid issuer address."); diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs index 9863331aff..406e41f8b7 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs @@ -6,17 +6,52 @@ public partial class TokenContract { private SymbolType GetCreateInputSymbolType(string symbol) { - var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); - Assert(words[0].Length > 0 && words[0].All(IsValidCreateSymbolChar), "Invalid Symbol input"); - if (words.Length == 1) return SymbolType.Token; - Assert(words.Length >= 2, "Invalid NFT Symbol input"); - for (var i = 1; i < words.Length; ++i) + var splitSymbols = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + + AssertTokenSymbolIsValid(splitSymbols); + + if (splitSymbols.Length == 1) return SymbolType.Token; + + AssertNFTSymbolIsValid(splitSymbols); + + // if we want to allow sub nft collection to be created with a different owner, we can check against words[^1] + return splitSymbols[1] == TokenContractConstants.CollectionSymbolSuffix ? SymbolType.NftCollection : SymbolType.Nft; + } + + private void AssertSymbolIsValid(string symbol) + { + var splitSymbols = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + AssertTokenSymbolIsValid(splitSymbols); + if (splitSymbols.Length == 1) return; + AssertNFTSymbolIsValid(splitSymbols); + } + + private void AssertTokenSymbolIsValid(string symbol) + { + var splitSymbols = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + AssertTokenSymbolIsValid(splitSymbols); + } + + private void AssertTokenSymbolIsValid(string[] splitSymbols) + { + Assert(splitSymbols[0].Length > 0 && splitSymbols[0].All(IsValidCreateSymbolChar), "Invalid Symbol input"); + } + + private void AssertNFTSymbolIsValid(string symbol) + { + var splitSymbols = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + AssertNFTSymbolIsValid(splitSymbols); + } + + private void AssertNFTSymbolIsValid(string[] splitSymbols) + { + Assert(splitSymbols.Length >= 2, "Invalid NFT Symbol input"); + Assert(splitSymbols[1].Length > 0 && splitSymbols[1].All(IsValidItemIdChar), "Invalid NFT Symbol input"); + for (var i = 2; i < splitSymbols.Length; ++i) { - var word = words[i]; - Assert(word.Length > 0 && word.All(IsValidItemIdChar), "Invalid NFT Symbol input"); + var word = splitSymbols[i]; + Assert(word.Length > 0 && word.All(IsValidSubItemIdChar), "Invalid NFT Symbol input"); } - // if we want to allow sub nft collection to be created with a different owner, we can check against words[^1] - return words[1] == TokenContractConstants.CollectionSymbolSuffix ? SymbolType.NftCollection : SymbolType.Nft; } private void AssertNFTCreateInput(CreateInput input) diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs index d8c354915e..990c7a32b0 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs @@ -91,12 +91,9 @@ private string GetNftCollectionSymbol(string inputSymbol) var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); const int tokenSymbolLength = 1; if (words.Length == tokenSymbolLength) return null; - Assert(words.Length >= 2, "Invalid NFT symbol input"); - for (var i = 1; i < words.Length; ++i) - { - var word = words[i]; - Assert(word.Length > 0 && word.All(IsValidItemIdChar), "Invalid NFT Symbol input"); - } + + AssertNFTSymbolIsValid(words); + return symbol == $"{words[0]}-0" ? null : $"{words[0]}-0"; } From 69bb17ab5ae349b8d3e58f2d0e20cf33e47107a1 Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Tue, 20 Feb 2024 23:45:14 +0800 Subject: [PATCH 3/4] feat(nft validity check): fixed check for digits and limit digits to not start with 0 for sub ids --- contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs | 7 ------- .../AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs index bc2f6c7db3..36acb75f59 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs @@ -18,13 +18,6 @@ private bool IsValidItemIdChar(char character) { return character >= '0' && character <= '9'; } - - // For checking if a sub item id is valid - // e.g. AELFIE-1-1337 is valid but AELFIE-1-0 is invalid as 0 represents a collection and there should not be any collection in a sub item - private bool IsValidSubItemIdChar(char character) - { - return character >= '1' && character <= '9'; - } private bool IsValidCreateSymbolChar(char character) { diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs index 406e41f8b7..0289c976dd 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs @@ -50,7 +50,7 @@ private void AssertNFTSymbolIsValid(string[] splitSymbols) for (var i = 2; i < splitSymbols.Length; ++i) { var word = splitSymbols[i]; - Assert(word.Length > 0 && word.All(IsValidSubItemIdChar), "Invalid NFT Symbol input"); + Assert(word.Length > 0 && word[0] != '0' && word.All(IsValidItemIdChar), "Invalid NFT Symbol input"); } } From 445d56a6a62b15060c40991024afcae10d130f8e Mon Sep 17 00:00:00 2001 From: zhifenglee-aelf Date: Tue, 20 Feb 2024 23:52:20 +0800 Subject: [PATCH 4/4] test(nft sub id test): added sub id test --- .../BVT/NftApplicationTests.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/NftApplicationTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/NftApplicationTests.cs index 24055b432b..80e682ee14 100644 --- a/test/AElf.Contracts.MultiToken.Tests/BVT/NftApplicationTests.cs +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/NftApplicationTests.cs @@ -114,6 +114,72 @@ public partial class MultiTokenContractTests } } }; + + private TokenInfo SubNft1155Info => new() + { + Symbol = "1-12419", + TokenName = "Trump Digital Trading Card #12419", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = Accounts[0].Address, + IssueChainId = _chainId, + IsBurnable = false, + ExternalInfo = new ExternalInfo() + { + Value = + { + { + NftInfoMetaFields.ImageUrlKey, + "https://i.seadn.io/gcs/files/0f5cdfaaf687de2ebb5834b129a5bef3.png?auto=format&w=3840" + }, + { NftInfoMetaFields.IsBurnedKey, "false" } + } + } + }; + + private TokenInfo LongIdSubNft1155Info => new() + { + Symbol = "1-19-153-35-8-90-23-4-15-66", + TokenName = "Trump Digital Trading Card #1-19-153-35-8-90-23-4-15-60", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = Accounts[0].Address, + IssueChainId = _chainId, + IsBurnable = false, + ExternalInfo = new ExternalInfo() + { + Value = + { + { + NftInfoMetaFields.ImageUrlKey, + "https://i.seadn.io/gcs/files/0f5cdfaaf687de2ebb5834b129a5bef3.png?auto=format&w=3840" + }, + { NftInfoMetaFields.IsBurnedKey, "false" } + } + } + }; + + private TokenInfo ErroneousSubNft1155Info => new() + { + Symbol = "1-0", + TokenName = "Trump Digital Trading Card #12419", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = Accounts[0].Address, + IssueChainId = _chainId, + IsBurnable = false, + ExternalInfo = new ExternalInfo() + { + Value = + { + { + NftInfoMetaFields.ImageUrlKey, + "https://i.seadn.io/gcs/files/0f5cdfaaf687de2ebb5834b129a5bef3.png?auto=format&w=3840" + }, + { NftInfoMetaFields.IsBurnedKey, "false" } + } + } + }; private async Task> CreateNftCollectionAsync(TokenInfo collectionInfo) { @@ -282,6 +348,87 @@ public async Task MultiTokenContract_Create_NFTCollection_Input_Check_Test() result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); } } + + [Fact(DisplayName = "[MultiToken_Nft] Create sub nft with valid id")] + public async Task MultiTokenContract_Create_Sub_Nft_With_Valid_Id_Test() + { + var symbols = new List(); + var collectionInfo = NftCollection1155Info; + var createCollectionRes = await CreateNftCollectionAsync(collectionInfo); + createCollectionRes.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + var collectionSymbolWords = AssertCreateCollection(createCollectionRes, collectionInfo, symbols); + + var createNft2Res = await CreateNftAsync(collectionInfo.Symbol, SubNft1155Info); + createNft2Res.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + var createNft2Log = TokenCreated.Parser.ParseFrom(createNft2Res.TransactionResult.Logs + .First(l => l.Name == nameof(TokenCreated)).NonIndexed); + var nft2SymbolWords = createNft2Log.Symbol.Split("-"); + Assert.True(nft2SymbolWords.Length == 3); + Assert.Equal(nft2SymbolWords[0], collectionSymbolWords[0]); + AssertTokenEqual(createNft2Log, SubNft1155Info); + symbols.Add(createNft2Log.Symbol); + createNft2Log.Symbol.ShouldBe(collectionInfo.Symbol + SubNft1155Info.Symbol); + } + + private string[] AssertCreateCollection(IExecutionResult createCollectionRes, TokenInfo collectionInfo, List symbols) + { + var createCollectionLog = TokenCreated.Parser.ParseFrom(createCollectionRes.TransactionResult.Logs + .First(l => l.Name == nameof(TokenCreated)).NonIndexed); + var collectionSymbolWords = createCollectionLog.Symbol.Split("-"); + Assert.True(collectionSymbolWords.Length == 2); + AssertTokenEqual(createCollectionLog, collectionInfo); + symbols.Add(createCollectionLog.Symbol); + createCollectionLog.Symbol.ShouldBe(collectionInfo.Symbol + "0"); + return collectionSymbolWords; + } + + [Fact(DisplayName = "[MultiToken_Nft] Create sub nft with long valid id")] + public async Task MultiTokenContract_Create_Sub_Nft_With_Long_Valid_Id_Test() + { + var symbols = new List(); + var collectionInfo = NftCollection1155Info; + var createCollectionRes = await CreateNftCollectionAsync(collectionInfo); + createCollectionRes.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + var collectionSymbolWords = AssertCreateCollection(createCollectionRes, collectionInfo, symbols); + + var createNft2Res = await CreateNftAsync(collectionInfo.Symbol, LongIdSubNft1155Info); + createNft2Res.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + var createNft2Log = TokenCreated.Parser.ParseFrom(createNft2Res.TransactionResult.Logs + .First(l => l.Name == nameof(TokenCreated)).NonIndexed); + var nft2SymbolWords = createNft2Log.Symbol.Split("-"); + Assert.True(nft2SymbolWords.Length == 11); + Assert.Equal(nft2SymbolWords[0], collectionSymbolWords[0]); + AssertTokenEqual(createNft2Log, LongIdSubNft1155Info); + symbols.Add(createNft2Log.Symbol); + createNft2Log.Symbol.ShouldBe(collectionInfo.Symbol + LongIdSubNft1155Info.Symbol); + } + + [Fact(DisplayName = "[MultiToken_Nft] Create sub nft with invalid id of 0")] + public async Task MultiTokenContract_Create_Sub_Nft_With_Invalid_Id_Test() + { + var symbols = new List(); + var collectionInfo = NftCollection1155Info; + var createCollectionRes = await CreateNftCollectionAsync(collectionInfo); + createCollectionRes.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + AssertCreateCollection(createCollectionRes, collectionInfo, symbols); + + var createInput = new CreateInput + { + Symbol = $"{collectionInfo.Symbol}{ErroneousSubNft1155Info.Symbol}", + TokenName = ErroneousSubNft1155Info.TokenName, + TotalSupply = ErroneousSubNft1155Info.TotalSupply, + Decimals = ErroneousSubNft1155Info.Decimals, + Issuer = ErroneousSubNft1155Info.Issuer, + IsBurnable = ErroneousSubNft1155Info.IsBurnable, + IssueChainId = ErroneousSubNft1155Info.IssueChainId, + ExternalInfo = ErroneousSubNft1155Info.ExternalInfo, + Owner = ErroneousSubNft1155Info.Issuer + }; + + var result = await TokenContractStub.Create.SendWithExceptionAsync(createInput);; + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + result.TransactionResult.Error.ShouldContain("Invalid NFT Symbol input"); + } [Fact(DisplayName = "[MultiToken_Nft] Create nft input check")] public async Task MultiTokenContract_Create_NFT_Input_Check_Test()