diff --git a/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs b/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs index 8a41b78140..50e0c8c092 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContractConstants.cs @@ -14,6 +14,7 @@ public static class TokenContractConstants public const string LockCallbackExternalInfoKey = "aelf_lock_callback"; public const string UnlockCallbackExternalInfoKey = "aelf_unlock_callback"; public const string LogEventExternalInfoKey = "aelf_log_event"; + public const string TokenAliasExternalInfoKey = "aelf_token_alias"; public const int DELEGATEE_MAX_COUNT = 24; public const char NFTSymbolSeparator = '-'; public const int NFTSymbolMaxLength = 30; @@ -24,4 +25,6 @@ public static class TokenContractConstants public const string SeedExpireTimeExternalInfoKey = "__seed_exp_time"; public const string NftCreateChainIdExternalInfoKey = "__nft_create_chain_id"; public const int DefaultMaxBatchApproveCount = 100; + public const char AllSymbolIdentifier = '*'; + } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContractState.cs b/contract/AElf.Contracts.MultiToken/TokenContractState.cs index aa6953a530..6627c3625b 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContractState.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContractState.cs @@ -8,6 +8,11 @@ public partial class TokenContractState : ContractState public StringState NativeTokenSymbol { get; set; } public StringState ChainPrimaryTokenSymbol { get; set; } + + /// + /// WARNING: Use GetTokenInfo & SetTokenInfo to operate TokenInfos + /// due to token symbol alias feature. + /// public MappedState TokenInfos { get; set; } public MappedState InsensitiveTokenExisting { get; set; } public MappedState SymbolSeedMap { get; set; } @@ -68,4 +73,7 @@ public partial class TokenContractState : ContractState public SingletonState TokenIssuerAndOwnerModificationDisabled { get; set; } public SingletonState MaxBatchApproveCount { get; set; } + + // Alias -> Actual Symbol + public MappedState SymbolAliasMap { get; set; } } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_ACS1_MethodFeeProvider.cs b/contract/AElf.Contracts.MultiToken/TokenContract_ACS1_MethodFeeProvider.cs index 9ef198b39c..e24c2d8acc 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_ACS1_MethodFeeProvider.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_ACS1_MethodFeeProvider.cs @@ -123,9 +123,12 @@ private bool CheckOrganizationExist(AuthorityInfo authorityInfo) private void AssertValidFeeToken(string symbol, long amount) { AssertValidSymbolAndAmount(symbol, amount); - if (State.TokenInfos[symbol] == null) + var tokenInfo = GetTokenInfo(symbol); + if (tokenInfo == null) + { throw new AssertionException("Token is not found"); - Assert(State.TokenInfos[symbol].IsBurnable, $"Token {symbol} cannot set as method fee."); + } + Assert(tokenInfo.IsBurnable, $"Token {symbol} cannot set as method fee."); } #endregion diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_ACS2_StatePathsProvider.cs b/contract/AElf.Contracts.MultiToken/TokenContract_ACS2_StatePathsProvider.cs index 8d12f604e1..8d013d3180 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_ACS2_StatePathsProvider.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_ACS2_StatePathsProvider.cs @@ -44,8 +44,6 @@ public override ResourceInfo GetResourceInfo(Transaction txn) { WritePaths = { - GetPath(nameof(TokenContractState.Allowances), args.From.ToString(), txn.From.ToString(), - args.Symbol), GetPath(nameof(TokenContractState.Balances), args.From.ToString(), args.Symbol), GetPath(nameof(TokenContractState.Balances), args.To.ToString(), args.Symbol), GetPath(nameof(TokenContractState.LockWhiteLists), args.Symbol, txn.From.ToString()) @@ -57,7 +55,7 @@ public override ResourceInfo GetResourceInfo(Transaction txn) GetPath(nameof(TokenContractState.TransactionFeeFreeAllowancesSymbolList)) } }; - + AddPathForAllowance(resourceInfo, args.From.ToString(), txn.From.ToString(), args.Symbol); AddPathForTransactionFee(resourceInfo, txn.From.ToString(), txn.MethodName); AddPathForDelegatees(resourceInfo, txn.From, txn.To, txn.MethodName); AddPathForTransactionFeeFreeAllowance(resourceInfo, txn.From); @@ -70,6 +68,18 @@ public override ResourceInfo GetResourceInfo(Transaction txn) } } + private void AddPathForAllowance(ResourceInfo resourceInfo, string from, string spender, string symbol) + { + resourceInfo.WritePaths.Add(GetPath(nameof(TokenContractState.Allowances), from, spender, symbol)); + resourceInfo.WritePaths.Add(GetPath(nameof(TokenContractState.Allowances), from, spender, + GetAllSymbolIdentifier())); + var symbolType = GetSymbolType(symbol); + if (symbolType == SymbolType.Nft || symbolType == SymbolType.NftCollection) + { + resourceInfo.WritePaths.Add(GetPath(nameof(TokenContractState.Allowances), from, spender, + GetNftCollectionAllSymbolIdentifier(symbol))); + } + } private void AddPathForTransactionFee(ResourceInfo resourceInfo, string from, string methodName) { diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs index 65981eee66..6adcf01a2a 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs @@ -32,7 +32,7 @@ public override Empty InitializeFromParentChain(InitializeFromParentChainInput i /// public override Empty Create(CreateInput input) { - var inputSymbolType = GetCreateInputSymbolType(input.Symbol); + var inputSymbolType = GetSymbolType(input.Symbol); if (input.Owner == null) { input.Owner = input.Issuer; @@ -77,6 +77,14 @@ private Empty CreateToken(CreateInput input, SymbolType symbolType = SymbolType. ExternalInfo = input.ExternalInfo ?? new ExternalInfo(), Owner = input.Owner }; + + if (IsAliasSettingExists(tokenInfo)) + { + Assert(symbolType == SymbolType.NftCollection, "Token alias can only be set for NFT Item."); + SetTokenAlias(tokenInfo); + } + + CheckTokenExists(tokenInfo.Symbol); RegisterTokenInfo(tokenInfo); if (string.IsNullOrEmpty(State.NativeTokenSymbol.Value)) { @@ -110,7 +118,7 @@ private Empty CreateToken(CreateInput input, SymbolType symbolType = SymbolType. private void CheckSeedNFT(string symbolSeed, String symbol) { Assert(!string.IsNullOrEmpty(symbolSeed), "Seed NFT does not exist."); - var tokenInfo = State.TokenInfos[symbolSeed]; + var tokenInfo = GetTokenInfo(symbolSeed); Assert(tokenInfo != null, "Seed NFT does not exist."); Assert(State.Balances[Context.Sender][symbolSeed] > 0, "Seed NFT balance is not enough."); Assert(tokenInfo.ExternalInfo != null && tokenInfo.ExternalInfo.Value.TryGetValue( @@ -131,7 +139,7 @@ private void CheckSeedNFT(string symbolSeed, String symbol) public override Empty SetPrimaryTokenSymbol(SetPrimaryTokenSymbolInput input) { Assert(State.ChainPrimaryTokenSymbol.Value == null, "Failed to set primary token symbol."); - Assert(!string.IsNullOrWhiteSpace(input.Symbol) && State.TokenInfos[input.Symbol] != null, "Invalid input symbol."); + Assert(!string.IsNullOrWhiteSpace(input.Symbol) && GetTokenInfo(input.Symbol) != null, "Invalid input symbol."); State.ChainPrimaryTokenSymbol.Value = input.Symbol; Context.Fire(new ChainPrimaryTokenSymbolSet { TokenSymbol = input.Symbol }); @@ -156,7 +164,7 @@ public override Empty Issue(IssueInput input) tokenInfo.Supply = tokenInfo.Supply.Add(input.Amount); Assert(tokenInfo.Issued <= tokenInfo.TotalSupply, "Total supply exceeded"); - State.TokenInfos[input.Symbol] = tokenInfo; + SetTokenInfo(tokenInfo); ModifyBalance(input.To, input.Symbol, input.Amount); Context.Fire(new Issued @@ -171,14 +179,14 @@ public override Empty Issue(IssueInput input) public override Empty Transfer(TransferInput input) { - AssertValidToken(input.Symbol, input.Amount); - DoTransfer(Context.Sender, input.To, input.Symbol, input.Amount, input.Memo); + var tokenInfo = AssertValidToken(input.Symbol, input.Amount); + DoTransfer(Context.Sender, input.To, tokenInfo.Symbol, input.Amount, input.Memo); DealWithExternalInfoDuringTransfer(new TransferFromInput { From = Context.Sender, To = input.To, Amount = input.Amount, - Symbol = input.Symbol, + Symbol = tokenInfo.Symbol, Memo = input.Memo }); return new Empty(); @@ -245,27 +253,29 @@ public override Empty Unlock(UnlockInput input) public override Empty TransferFrom(TransferFromInput input) { - AssertValidToken(input.Symbol, input.Amount); - DoTransferFrom(input.From, input.To, Context.Sender, input.Symbol, input.Amount, input.Memo); + var tokenInfo = AssertValidToken(input.Symbol, input.Amount); + DoTransferFrom(input.From, input.To, Context.Sender, tokenInfo.Symbol, input.Amount, input.Memo); return new Empty(); } public override Empty Approve(ApproveInput input) { AssertValidInputAddress(input.Spender); - AssertValidToken(input.Symbol, input.Amount); - Approve(input.Spender, input.Symbol, input.Amount); + var actualSymbol = GetActualTokenSymbol(input.Symbol); + AssertValidApproveTokenAndAmount(actualSymbol, input.Amount); + Approve(input.Spender, actualSymbol, input.Amount); return new Empty(); } private void Approve(Address spender, string symbol, long amount) { - State.Allowances[Context.Sender][spender][symbol] = amount; + var actualSymbol = GetActualTokenSymbol(symbol); + State.Allowances[Context.Sender][spender][actualSymbol] = amount; Context.Fire(new Approved { Owner = Context.Sender, Spender = spender, - Symbol = symbol, + Symbol = actualSymbol, Amount = amount }); } @@ -277,7 +287,8 @@ public override Empty BatchApprove(BatchApproveInput input) foreach (var approve in input.Value) { AssertValidInputAddress(approve.Spender); - AssertValidToken(approve.Symbol, approve.Amount); + var actualSymbol = GetActualTokenSymbol(approve.Symbol); + AssertValidApproveTokenAndAmount(actualSymbol, approve.Amount); } var approveInputList = input.Value.GroupBy(approve => approve.Symbol + approve.Spender, approve => approve) .Select(approve => approve.Last()).ToList(); @@ -289,15 +300,16 @@ public override Empty BatchApprove(BatchApproveInput input) public override Empty UnApprove(UnApproveInput input) { AssertValidInputAddress(input.Spender); - AssertValidToken(input.Symbol, input.Amount); - var oldAllowance = State.Allowances[Context.Sender][input.Spender][input.Symbol]; + var symbol = GetActualTokenSymbol(input.Symbol); + AssertValidApproveTokenAndAmount(symbol, input.Amount); + var oldAllowance = State.Allowances[Context.Sender][input.Spender][symbol]; var amountOrAll = Math.Min(input.Amount, oldAllowance); - State.Allowances[Context.Sender][input.Spender][input.Symbol] = oldAllowance.Sub(amountOrAll); + State.Allowances[Context.Sender][input.Spender][symbol] = oldAllowance.Sub(amountOrAll); Context.Fire(new UnApproved { Owner = Context.Sender, Spender = input.Spender, - Symbol = input.Symbol, + Symbol = symbol, Amount = amountOrAll }); return new Empty(); @@ -426,7 +438,7 @@ public override Empty TakeResourceTokenBack(TakeResourceTokenBackInput input) public override Empty ValidateTokenInfoExists(ValidateTokenInfoExistsInput input) { Assert(!string.IsNullOrWhiteSpace(input.Symbol), "Invalid input symbol."); - var tokenInfo = State.TokenInfos[input.Symbol]; + var tokenInfo = GetTokenInfo(input.Symbol); if (tokenInfo == null) throw new AssertionException("Token validation failed."); var validationResult = tokenInfo.TokenName == input.TokenName && @@ -489,24 +501,38 @@ public override Empty CrossChainCreateToken(CrossChainCreateTokenInput input) ExternalInfo = new ExternalInfo { Value = { validateTokenInfoExistsInput.ExternalInfo } }, Owner = validateTokenInfoExistsInput.Owner ?? validateTokenInfoExistsInput.Issuer }; - RegisterTokenInfo(tokenInfo); - Context.Fire(new TokenCreated + + var isSymbolAliasSet = SyncSymbolAliasFromTokenInfo(tokenInfo); + if (State.TokenInfos[tokenInfo.Symbol] == null) { - Symbol = validateTokenInfoExistsInput.Symbol, - TokenName = validateTokenInfoExistsInput.TokenName, - TotalSupply = validateTokenInfoExistsInput.TotalSupply, - Decimals = validateTokenInfoExistsInput.Decimals, - Issuer = validateTokenInfoExistsInput.Issuer, - IsBurnable = validateTokenInfoExistsInput.IsBurnable, - IssueChainId = validateTokenInfoExistsInput.IssueChainId, - ExternalInfo = new ExternalInfo { Value = { validateTokenInfoExistsInput.ExternalInfo } }, - Owner = tokenInfo.Owner - }); + RegisterTokenInfo(tokenInfo); + Context.Fire(new TokenCreated + { + Symbol = validateTokenInfoExistsInput.Symbol, + TokenName = validateTokenInfoExistsInput.TokenName, + TotalSupply = validateTokenInfoExistsInput.TotalSupply, + Decimals = validateTokenInfoExistsInput.Decimals, + Issuer = validateTokenInfoExistsInput.Issuer, + IsBurnable = validateTokenInfoExistsInput.IsBurnable, + IssueChainId = validateTokenInfoExistsInput.IssueChainId, + ExternalInfo = new ExternalInfo { Value = { validateTokenInfoExistsInput.ExternalInfo } }, + Owner = tokenInfo.Owner, + }); + } + else + { + if (isSymbolAliasSet && + validateTokenInfoExistsInput.ExternalInfo.TryGetValue(TokenContractConstants.TokenAliasExternalInfoKey, + out var tokenAliasSetting)) + { + State.TokenInfos[tokenInfo.Symbol].ExternalInfo.Value + .Add(TokenContractConstants.TokenAliasExternalInfoKey, tokenAliasSetting); + } + } return new Empty(); } - public override Empty RegisterCrossChainTokenContractAddress(RegisterCrossChainTokenContractAddressInput input) { CheckCrossChainTokenContractRegistrationControllerAuthority(); @@ -533,21 +559,21 @@ public override Empty RegisterCrossChainTokenContractAddress(RegisterCrossChainT /// public override Empty CrossChainTransfer(CrossChainTransferInput input) { - AssertValidToken(input.Symbol, input.Amount); + var tokenInfo = AssertValidToken(input.Symbol, input.Amount); AssertValidMemo(input.Memo); - var issueChainId = GetIssueChainId(input.Symbol); + var issueChainId = GetIssueChainId(tokenInfo.Symbol); Assert(issueChainId == input.IssueChainId, "Incorrect issue chain id."); var burnInput = new BurnInput { Amount = input.Amount, - Symbol = input.Symbol + Symbol = tokenInfo.Symbol }; Burn(burnInput); Context.Fire(new CrossChainTransferred { From = Context.Sender, To = input.To, - Symbol = input.Symbol, + Symbol = tokenInfo.Symbol, Amount = input.Amount, IssueChainId = input.IssueChainId, Memo = input.Memo, @@ -578,28 +604,28 @@ public override Empty CrossChainReceiveToken(CrossChainReceiveTokenInput input) var transferSender = transferTransaction.From; var tokenInfo = AssertValidToken(symbol, amount); - var issueChainId = GetIssueChainId(symbol); + var issueChainId = GetIssueChainId(tokenInfo.Symbol); Assert(issueChainId == crossChainTransferInput.IssueChainId, "Incorrect issue chain id."); Assert(targetChainId == Context.ChainId, "Unable to claim cross chain token."); var registeredTokenContractAddress = State.CrossChainTransferWhiteList[input.FromChainId]; AssertCrossChainTransaction(transferTransaction, registeredTokenContractAddress, nameof(CrossChainTransfer)); Context.LogDebug(() => - $"symbol == {symbol}, amount == {amount}, receivingAddress == {receivingAddress}, targetChainId == {targetChainId}"); + $"symbol == {tokenInfo.Symbol}, amount == {amount}, receivingAddress == {receivingAddress}, targetChainId == {targetChainId}"); CrossChainVerify(transferTransactionId, input.ParentChainHeight, input.FromChainId, input.MerklePath); State.VerifiedCrossChainTransferTransaction[transferTransactionId] = true; tokenInfo.Supply = tokenInfo.Supply.Add(amount); Assert(tokenInfo.Supply <= tokenInfo.TotalSupply, "Total supply exceeded"); - State.TokenInfos[symbol] = tokenInfo; - ModifyBalance(receivingAddress, symbol, amount); + SetTokenInfo(tokenInfo); + ModifyBalance(receivingAddress, tokenInfo.Symbol, amount); Context.Fire(new CrossChainReceived { From = transferSender, To = receivingAddress, - Symbol = symbol, + Symbol = tokenInfo.Symbol, Amount = amount, Memo = crossChainTransferInput.Memo, FromChainId = input.FromChainId, @@ -619,7 +645,7 @@ public override Empty ModifyTokenIssuerAndOwner(ModifyTokenIssuerAndOwnerInput i Assert(input.Issuer != null && !input.Issuer.Value.IsNullOrEmpty(), "Invalid input issuer."); Assert(input.Owner != null && !input.Owner.Value.IsNullOrEmpty(), "Invalid input owner."); - var tokenInfo = State.TokenInfos[input.Symbol]; + var tokenInfo = GetTokenInfo(input.Symbol); Assert(tokenInfo != null, "Token is not found."); Assert(tokenInfo.Issuer == Context.Sender, "Only token issuer can set token issuer and owner."); @@ -648,7 +674,7 @@ public override BoolValue GetTokenIssuerAndOwnerModificationEnabled(Empty input) Value = !State.TokenIssuerAndOwnerModificationDisabled.Value }; } - + public override Empty SetMaxBatchApproveCount(Int32Value input) { Assert(input.Value > 0, "Invalid input."); @@ -671,4 +697,127 @@ private int GetMaxBatchApproveCount() ? TokenContractConstants.DefaultMaxBatchApproveCount : State.MaxBatchApproveCount.Value; } + + /// + /// For example: + /// Symbol: SGR-1, Alias: SGR + /// Symbol: ABC-233, Alias: ABC + /// + /// + /// + public override Empty SetSymbolAlias(SetSymbolAliasInput input) + { + // Alias setting can only work for NFT Item for now. + // And the setting exists on the TokenInfo of the NFT Collection. + + // Can only happen on Main Chain. + Assert(Context.ChainId == ChainHelper.ConvertBase58ToChainId("AELF"), + "Symbol alias setting only works on MainChain."); + + var collectionSymbol = GetNftCollectionSymbol(input.Symbol, true); + + // For now, token alias can only be set once. + Assert(State.SymbolAliasMap[input.Alias] == null, $"Token alias {input.Alias} already exists."); + + CheckTokenAlias(input.Alias, collectionSymbol); + + var collectionTokenInfo = GetTokenInfo(collectionSymbol); + if (collectionTokenInfo == null) + { + throw new AssertionException($"NFT Collection {collectionSymbol} not found."); + } + + Assert(collectionTokenInfo.Owner == Context.Sender || collectionTokenInfo.Issuer == Context.Sender, + "No permission."); + + collectionTokenInfo.ExternalInfo.Value[TokenContractConstants.TokenAliasExternalInfoKey] + = $"{{\"{input.Symbol}\":\"{input.Alias}\"}}"; + + SetTokenInfo(collectionTokenInfo); + + State.SymbolAliasMap[input.Alias] = input.Symbol; + + Context.LogDebug(() => $"Token alias added: {input.Symbol} -> {input.Alias}"); + + Context.Fire(new SymbolAliasAdded + { + Symbol = input.Symbol, + Alias = input.Alias + }); + + return new Empty(); + } + + private bool SyncSymbolAliasFromTokenInfo(TokenInfo newTokenInfo) + { + var maybePreviousTokenInfo = State.TokenInfos[newTokenInfo.Symbol]?.Clone(); + + if (maybePreviousTokenInfo != null && IsAliasSettingExists(maybePreviousTokenInfo)) + { + return false; + } + + if (IsAliasSettingExists(newTokenInfo)) + { + SetTokenAlias(newTokenInfo); + return true; + } + + return false; + } + + private bool IsAliasSettingExists(TokenInfo tokenInfo) + { + return tokenInfo.ExternalInfo != null && + tokenInfo.ExternalInfo.Value.Count > 0 && + tokenInfo.ExternalInfo.Value.ContainsKey(TokenContractConstants.TokenAliasExternalInfoKey); + } + + /// + /// Extract alias setting from ExternalInfo. + /// + /// + /// (Symbol, Alias) + private KeyValuePair ExtractAliasSetting(TokenInfo tokenInfo) + { + if (!tokenInfo.ExternalInfo.Value.ContainsKey(TokenContractConstants.TokenAliasExternalInfoKey)) + { + return new KeyValuePair(string.Empty, string.Empty); + } + + var tokenAliasSetting = tokenInfo.ExternalInfo.Value[TokenContractConstants.TokenAliasExternalInfoKey]; + tokenAliasSetting = tokenAliasSetting.Trim('{', '}'); + var parts = tokenAliasSetting.Split(':'); + var key = parts[0].Trim().Trim('\"'); + var value = parts[1].Trim().Trim('\"'); + return new KeyValuePair(key, value); + } + + private void SetTokenAlias(TokenInfo tokenInfo) + { + var (symbol, alias) = ExtractAliasSetting(tokenInfo); + State.SymbolAliasMap[alias] = symbol; + + CheckTokenAlias(alias, tokenInfo.Symbol); + + Context.Fire(new SymbolAliasAdded + { + Symbol = symbol, + Alias = alias + }); + } + + private void CheckTokenAlias(string alias, string collectionSymbol) + { + if (collectionSymbol == null) + { + throw new AssertionException("Token alias can only be set for NFT Item."); + } + + // Current Rule: Alias must be the seed name. + var parts = collectionSymbol.Split(TokenContractConstants.NFTSymbolSeparator); + Assert(parts.Length == 2, $"Incorrect collection symbol: {collectionSymbol}."); + Assert(parts.Last() == TokenContractConstants.CollectionSymbolSuffix, "Incorrect collection symbol suffix."); + Assert(alias == parts.First(), $"Alias for an item of {collectionSymbol} cannot be {alias}."); + } } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Fees.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Fees.cs index 554ca1c480..12743edd91 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Fees.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Fees.cs @@ -620,7 +620,6 @@ public override Empty SetSymbolsToPayTxSizeFee(SymbolListToPayTxSizeFee input) var isPrimaryTokenExist = false; var symbolList = new List(); var primaryTokenSymbol = GetPrimaryTokenSymbol(new Empty()); - var primaryTokenInfo = State.TokenInfos[primaryTokenSymbol.Value]; Assert(!string.IsNullOrEmpty(primaryTokenSymbol.Value), "primary token does not exist"); foreach (var tokenWeightInfo in input.SymbolsToPayTxSizeFee) { @@ -1153,7 +1152,7 @@ private void TransferTransactionFeesToFeeReceiver(string symbol, long totalAmoun if (totalAmount <= 0) return; - var tokenInfo = State.TokenInfos[symbol]; + var tokenInfo = GetTokenInfo(symbol); if (!tokenInfo.IsBurnable) { return; @@ -1268,7 +1267,7 @@ public override Empty ConfigTransactionFeeFreeAllowances(ConfigTransactionFeeFre private void ValidateToken(string symbol) { Assert(!string.IsNullOrWhiteSpace(symbol), "Invalid input symbol"); - Assert(State.TokenInfos[symbol] != null, $"Symbol {symbol} not exist"); + Assert(GetTokenInfo(symbol) != null, $"Symbol {symbol} not exist"); } public override Empty RemoveTransactionFeeFreeAllowancesConfig(RemoveTransactionFeeFreeAllowancesConfigInput input) @@ -1411,7 +1410,7 @@ private bool IsDelegationEnough(string txSymbol, string baseSymbol, long cost, private void AssertSymbolToPayTxFeeIsValid(string tokenSymbol, out long totalSupply) { - var tokenInfo = State.TokenInfos[tokenSymbol]; + var tokenInfo = GetTokenInfo(tokenSymbol); if (tokenInfo == null) { throw new AssertionException($"Token is not found. {tokenSymbol}"); diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs index 40616c8973..3a78c60cbe 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Helper.cs @@ -33,11 +33,51 @@ private bool IsValidCreateSymbol(string symbol) private TokenInfo AssertValidToken(string symbol, long amount) { AssertValidSymbolAndAmount(symbol, amount); - var tokenInfo = State.TokenInfos[symbol]; + var tokenInfo = GetTokenInfo(symbol); Assert(tokenInfo != null && !string.IsNullOrEmpty(tokenInfo.Symbol), $"Token is not found. {symbol}"); return tokenInfo; } + private void AssertValidApproveTokenAndAmount(string symbol, long amount) + { + Assert(amount > 0, "Invalid amount."); + AssertApproveToken(symbol); + } + + private void ValidTokenExists(string symbol) + { + var tokenInfo = State.TokenInfos[symbol]; + Assert(tokenInfo != null && !string.IsNullOrEmpty(tokenInfo.Symbol), + $"Token is not found. {symbol}"); + } + + private void AssertApproveToken(string symbol) + { + Assert(!string.IsNullOrEmpty(symbol), "Symbol can not be null."); + var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + var symbolPrefix = words[0]; + var allSymbolIdentifier = GetAllSymbolIdentifier(); + Assert(symbolPrefix.Length > 0 && (IsValidCreateSymbol(symbolPrefix) || symbolPrefix.Equals(allSymbolIdentifier)), "Invalid symbol."); + if (words.Length == 1) + { + if (!symbolPrefix.Equals(allSymbolIdentifier)) + { + ValidTokenExists(symbolPrefix); + } + return; + } + Assert(words.Length == 2, "Invalid symbol length."); + var itemId = words[1]; + Assert(itemId.Length > 0 && (IsValidItemId(itemId) || itemId.Equals(allSymbolIdentifier)), "Invalid NFT Symbol."); + var nftSymbol = itemId.Equals(allSymbolIdentifier) ? GetCollectionSymbol(symbolPrefix) : symbol; + ValidTokenExists(nftSymbol); + } + + private string GetCollectionSymbol(string symbolPrefix) + { + return $"{symbolPrefix}-{TokenContractConstants.CollectionSymbolSuffix}"; + } + private void AssertValidSymbolAndAmount(string symbol, long amount) { Assert(!string.IsNullOrEmpty(symbol) && IsValidSymbol(symbol), @@ -122,13 +162,12 @@ private List GetSymbolListSortedByExpirationTime(TransactionFeeFreeAllow fromAddress][t]).Seconds).ToList(); } - private long GetBalance(Address address, string symbol) { AssertValidInputAddress(address); - Assert(!string.IsNullOrWhiteSpace(symbol), "Invalid symbol."); - - return State.Balances[address][symbol]; + var actualSymbol = GetActualTokenSymbol(symbol); + Assert(!string.IsNullOrWhiteSpace(actualSymbol), "Invalid symbol."); + return State.Balances[address][actualSymbol]; } // private MethodFeeFreeAllowance GetFreeFeeAllowance(MethodFeeFreeAllowances freeAllowances, string symbol) @@ -183,7 +222,6 @@ private void AssertCrossChainTransaction(Transaction originalTransaction, Addres private void RegisterTokenInfo(TokenInfo tokenInfo) { - CheckTokenExists(tokenInfo.Symbol); Assert(!string.IsNullOrEmpty(tokenInfo.Symbol) && IsValidSymbol(tokenInfo.Symbol), "Invalid symbol."); Assert(!string.IsNullOrEmpty(tokenInfo.TokenName), "Token name can neither be null nor empty."); @@ -226,7 +264,7 @@ private AuthorityInfo GetCrossChainTokenContractRegistrationController() private int GetIssueChainId(string symbol) { - var tokenInfo = State.TokenInfos[symbol]; + var tokenInfo = GetTokenInfo(symbol); return tokenInfo.IssueChainId; } @@ -257,7 +295,7 @@ private void CheckTokenExists(string symbol) { var empty = new TokenInfo(); // check old token - var existing = State.TokenInfos[symbol.ToUpper()]; + var existing = GetTokenInfo(symbol); Assert(existing == null || existing.Equals(empty), "Token already exists."); // check new token Assert(!State.InsensitiveTokenExisting[symbol.ToUpper()], "Token already exists."); @@ -282,7 +320,7 @@ private void CheckCrossChainTokenContractRegistrationControllerAuthority() private void DealWithExternalInfoDuringLocking(TransferFromInput input) { - var tokenInfo = State.TokenInfos[input.Symbol]; + var tokenInfo = GetTokenInfo(input.Symbol); if (tokenInfo.ExternalInfo == null) return; if (tokenInfo.ExternalInfo.Value.ContainsKey(TokenContractConstants.LockCallbackExternalInfoKey)) { @@ -297,7 +335,7 @@ private void DealWithExternalInfoDuringLocking(TransferFromInput input) private void DealWithExternalInfoDuringTransfer(TransferFromInput input) { - var tokenInfo = State.TokenInfos[input.Symbol]; + var tokenInfo = GetTokenInfo(input.Symbol); if (tokenInfo.ExternalInfo == null) return; if (tokenInfo.ExternalInfo.Value.ContainsKey(TokenContractConstants.TransferCallbackExternalInfoKey)) { @@ -312,7 +350,7 @@ private void DealWithExternalInfoDuringTransfer(TransferFromInput input) private void DealWithExternalInfoDuringUnlock(TransferFromInput input) { - var tokenInfo = State.TokenInfos[input.Symbol]; + var tokenInfo = GetTokenInfo(input.Symbol); if (tokenInfo.ExternalInfo == null) return; if (tokenInfo.ExternalInfo.Value.ContainsKey(TokenContractConstants.UnlockCallbackExternalInfoKey)) { @@ -362,4 +400,23 @@ private Address GetVoteContractAddress() return State.VoteContractAddress.Value; } + + private TokenInfo GetTokenInfo(string symbolOrAlias) + { + var tokenInfo = State.TokenInfos[symbolOrAlias]; + if (tokenInfo != null) return tokenInfo; + var actualTokenSymbol = State.SymbolAliasMap[symbolOrAlias]; + if (!string.IsNullOrEmpty(actualTokenSymbol)) + { + tokenInfo = State.TokenInfos[actualTokenSymbol]; + } + + return tokenInfo; + } + + private void SetTokenInfo(TokenInfo tokenInfo) + { + var symbol = tokenInfo.Symbol; + State.TokenInfos[symbol] = tokenInfo; + } } \ No newline at end of file diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs index 63747f2784..ae01062131 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFTHelper.cs @@ -4,7 +4,7 @@ namespace AElf.Contracts.MultiToken; public partial class TokenContract { - private SymbolType GetCreateInputSymbolType(string symbol) + private SymbolType GetSymbolType(string symbol) { var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); Assert(words[0].Length > 0 && IsValidCreateSymbol(words[0]), "Invalid Symbol input"); diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs index 24fc1fc3ef..93d54ec7b3 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_NFT_Actions.cs @@ -43,7 +43,7 @@ private Empty CreateNFTInfo(CreateInput input) out var expirationTime) && long.TryParse(expirationTime, out var expirationTimeLong) && Context.CurrentBlockTime.Seconds <= expirationTimeLong, "Invalid ownedSymbol."); - var ownedSymbolType = GetCreateInputSymbolType(ownedSymbol); + var ownedSymbolType = GetSymbolType(ownedSymbol); Assert(ownedSymbolType != SymbolType.Nft, "Invalid OwnedSymbol."); CheckSymbolLength(ownedSymbol, ownedSymbolType); CheckTokenAndCollectionExists(ownedSymbol); @@ -58,7 +58,7 @@ private void CheckSymbolSeed(string ownedSymbol) { var oldSymbolSeed = State.SymbolSeedMap[ownedSymbol.ToUpper()]; - Assert(oldSymbolSeed == null || !State.TokenInfos[oldSymbolSeed].ExternalInfo.Value + Assert(oldSymbolSeed == null || !GetTokenInfo(oldSymbolSeed).ExternalInfo.Value .TryGetValue(TokenContractConstants.SeedExpireTimeExternalInfoKey, out var oldSymbolSeedExpireTime) || !long.TryParse(oldSymbolSeedExpireTime, out var symbolSeedExpireTime) @@ -66,14 +66,13 @@ private void CheckSymbolSeed(string ownedSymbol) "OwnedSymbol has been created"); } - private void DoTransferFrom(Address from, Address to, Address spender, string symbol, long amount, string memo) { AssertValidInputAddress(from); AssertValidInputAddress(to); // First check allowance. - var allowance = State.Allowances[from][spender][symbol]; + var allowance = GetAllowance(from, spender, symbol, amount, out var allowanceSymbol); if (allowance < amount) { if (IsInWhiteList(new IsInWhiteListInput { Symbol = symbol, Address = spender }).Value) @@ -92,25 +91,80 @@ private void DoTransferFrom(Address from, Address to, Address spender, string sy DoTransfer(from, to, symbol, amount, memo); DealWithExternalInfoDuringTransfer(new TransferFromInput() { From = from, To = to, Symbol = symbol, Amount = amount, Memo = memo }); - State.Allowances[from][spender][symbol] = allowance.Sub(amount); + State.Allowances[from][spender][allowanceSymbol] = allowance.Sub(amount); } + private long GetAllowance(Address from, Address spender, string sourceSymbol, long amount, + out string allowanceSymbol) + { + allowanceSymbol = sourceSymbol; + var allowance = State.Allowances[from][spender][sourceSymbol]; + if (allowance >= amount) return allowance; + var tokenType = GetSymbolType(sourceSymbol); + if (tokenType == SymbolType.Token) + { + allowance = GetAllSymbolAllowance(from, spender, out allowanceSymbol); + } + else + { + allowance = GetNftCollectionAllSymbolAllowance(from, spender, sourceSymbol, out allowanceSymbol); + if (allowance >= amount) return allowance; + allowance = GetAllSymbolAllowance(from, spender, out allowanceSymbol); + } + + return allowance; + } + + + private long GetAllSymbolAllowance(Address from, Address spender, out string allowanceSymbol) + { + allowanceSymbol = GetAllSymbolIdentifier(); + return State.Allowances[from][spender][allowanceSymbol]; + } + + private long GetNftCollectionAllSymbolAllowance(Address from, Address spender, string sourceSymbol, + out string allowanceSymbol) + { + allowanceSymbol = GetNftCollectionAllSymbolIdentifier(sourceSymbol); + return State.Allowances[from][spender][allowanceSymbol]; + } + + private string GetNftCollectionAllSymbolIdentifier(string sourceSymbol) + { + // "AAA-*" + return $"{sourceSymbol.Split(TokenContractConstants.NFTSymbolSeparator)[0]}-{TokenContractConstants.AllSymbolIdentifier}"; + } + + private string GetAllSymbolIdentifier() + { + // "*" + return TokenContractConstants.AllSymbolIdentifier.ToString(); + } - private string GetNftCollectionSymbol(string inputSymbol) + /// + /// ELF -> null + /// NFT-1 -> NFT-0 + /// If isAllowCollection == true: NFT-0 -> NFT-0 + /// If isAllowCollection == false: NFT-0 -> null + /// + /// + /// + /// Return null if inputSymbol is not NFT. + private string GetNftCollectionSymbol(string inputSymbol, bool isAllowCollection = false) { var symbol = inputSymbol; var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); const int tokenSymbolLength = 1; if (words.Length == tokenSymbolLength) return null; Assert(words.Length == 2 && IsValidItemId(words[1]), "Invalid NFT Symbol Input"); - return symbol == $"{words[0]}-0" ? null : $"{words[0]}-0"; + return symbol == $"{words[0]}-0" ? (isAllowCollection ? $"{words[0]}-0" : null) : $"{words[0]}-0"; } private TokenInfo AssertNftCollectionExist(string symbol) { var collectionSymbol = GetNftCollectionSymbol(symbol); if (collectionSymbol == null) return null; - var collectionInfo = State.TokenInfos[collectionSymbol]; + var collectionInfo = GetTokenInfo(collectionSymbol); Assert(collectionInfo != null, "NFT collection not exist"); return collectionInfo; } diff --git a/contract/AElf.Contracts.MultiToken/TokenContract_Views.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Views.cs index 8366261ebf..01eb2bd7be 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Views.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Views.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using AElf.Sdk.CSharp; using AElf.Types; using Google.Protobuf.WellKnownTypes; @@ -10,12 +11,12 @@ public partial class TokenContract [View] public override TokenInfo GetTokenInfo(GetTokenInfoInput input) { - return State.TokenInfos[input.Symbol]; + return GetTokenInfo(input.Symbol); } public override TokenInfo GetNativeTokenInfo(Empty input) { - return State.TokenInfos[State.NativeTokenSymbol.Value]; + return GetTokenInfo(State.NativeTokenSymbol.Value); } public override TokenInfoList GetResourceTokenInfo(Empty input) @@ -23,13 +24,13 @@ public override TokenInfoList GetResourceTokenInfo(Empty input) var tokenInfoList = new TokenInfoList(); foreach (var symbol in Context.Variables.GetStringArray(TokenContractConstants.PayTxFeeSymbolListName) .Where(symbol => - State.TokenInfos[symbol] != null)) - tokenInfoList.Value.Add(State.TokenInfos[symbol]); + GetTokenInfo(symbol) != null)) + tokenInfoList.Value.Add(GetTokenInfo(symbol)); foreach (var symbol in Context.Variables.GetStringArray(TokenContractConstants.PayRentalSymbolListName) .Where(symbol => - State.TokenInfos[symbol] != null)) - tokenInfoList.Value.Add(State.TokenInfos[symbol]); + GetTokenInfo(symbol) != null)) + tokenInfoList.Value.Add(GetTokenInfo(symbol)); return tokenInfoList; } @@ -37,26 +38,61 @@ public override TokenInfoList GetResourceTokenInfo(Empty input) [View] public override GetBalanceOutput GetBalance(GetBalanceInput input) { + var symbol = GetActualTokenSymbol(input.Symbol); return new GetBalanceOutput { Symbol = input.Symbol, Owner = input.Owner, - Balance = GetBalance(input.Owner, input.Symbol) + Balance = GetBalance(input.Owner, symbol) }; } - + [View] public override GetAllowanceOutput GetAllowance(GetAllowanceInput input) { + var symbol = GetActualTokenSymbol(input.Symbol); return new GetAllowanceOutput + { + Symbol = symbol, + Owner = input.Owner, + Spender = input.Spender, + Allowance = State.Allowances[input.Owner][input.Spender][symbol] + }; + } + + [View] + public override GetAllowanceOutput GetAvailableAllowance(GetAllowanceInput input) + { + var result = new GetAllowanceOutput { Symbol = input.Symbol, Owner = input.Owner, Spender = input.Spender, - Allowance = State.Allowances[input.Owner][input.Spender][input.Symbol] }; + var symbol = input.Symbol; + var allowance = State.Allowances[input.Owner][input.Spender][symbol]; + if (CheckSymbolIdentifier(symbol)) + { + result.Allowance = allowance; + return result; + } + var symbolType = GetSymbolType(symbol); + allowance = Math.Max(allowance, GetAllSymbolAllowance(input.Owner,input.Spender,out _)); + if (symbolType == SymbolType.Nft || symbolType == SymbolType.NftCollection) + { + allowance = Math.Max(allowance, GetNftCollectionAllSymbolAllowance(input.Owner, input.Spender, symbol, out _)); + } + result.Allowance = allowance; + return result; } + private bool CheckSymbolIdentifier(string symbol) + { + var words = symbol.Split(TokenContractConstants.NFTSymbolSeparator); + var allSymbolIdentifier = GetAllSymbolIdentifier(); + return words[0].Equals(allSymbolIdentifier) || (words.Length > 1 && words[1].Equals(allSymbolIdentifier)); + } + public override BoolValue IsInWhiteList(IsInWhiteListInput input) { return new BoolValue { Value = State.LockWhiteLists[input.Symbol][input.Address] }; @@ -199,7 +235,6 @@ public override BoolValue IsTokenAvailableForMethodFee(StringValue input) }; } - public override StringList GetReservedExternalInfoKeyList(Empty input) { return new StringList @@ -216,7 +251,7 @@ public override StringList GetReservedExternalInfoKeyList(Empty input) private bool IsTokenAvailableForMethodFee(string symbol) { - var tokenInfo = State.TokenInfos[symbol]; + var tokenInfo = GetTokenInfo(symbol); if (tokenInfo == null) throw new AssertionException("Token is not found."); return tokenInfo.IsBurnable; } @@ -228,4 +263,33 @@ private bool IsAddressInCreateWhiteList(Address address) address == Context.GetContractAddressByName(SmartContractConstants.EconomicContractSystemName) || address == Context.GetContractAddressByName(SmartContractConstants.CrossChainContractSystemName); } + + public override StringValue GetTokenAlias(StringValue input) + { + var collectionSymbol = GetNftCollectionSymbol(input.Value, true); + var tokenInfo = GetTokenInfo(collectionSymbol); + var (_, alias) = ExtractAliasSetting(tokenInfo); + return new StringValue + { + Value = alias + }; + } + + public override StringValue GetSymbolByAlias(StringValue input) + { + return new StringValue + { + Value = GetActualTokenSymbol(input.Value) + }; + } + + private string GetActualTokenSymbol(string aliasOrSymbol) + { + if (State.TokenInfos[aliasOrSymbol] == null) + { + return State.SymbolAliasMap[aliasOrSymbol] ?? aliasOrSymbol; + } + + return aliasOrSymbol; + } } \ No newline at end of file diff --git a/protobuf/token_contract.proto b/protobuf/token_contract.proto index e1174b8b45..9931b680a3 100644 --- a/protobuf/token_contract.proto +++ b/protobuf/token_contract.proto @@ -144,6 +144,9 @@ service TokenContract { rpc RemoveTransactionFeeDelegatee (RemoveTransactionFeeDelegateeInput) returns (google.protobuf.Empty){ } + rpc SetSymbolAlias (SetSymbolAliasInput) returns (google.protobuf.Empty){ + } + // Get all delegatees' address of delegator from input rpc GetTransactionFeeDelegatees (GetTransactionFeeDelegateesInput) returns (GetTransactionFeeDelegateesOutput) { option (aelf.is_view) = true; @@ -174,6 +177,11 @@ service TokenContract { option (aelf.is_view) = true; } + // Query the account's available allowance for other addresses + rpc GetAvailableAllowance (GetAllowanceInput) returns (GetAllowanceOutput) { + option (aelf.is_view) = true; + } + // Check whether the token is in the whitelist of an address, // which can be called TransferFrom to transfer the token under the condition of not being credited. rpc IsInWhiteList (IsInWhiteListInput) returns (google.protobuf.BoolValue) { @@ -229,6 +237,14 @@ service TokenContract { rpc GetTransactionFeeDelegationsOfADelegatee(GetTransactionFeeDelegationsOfADelegateeInput) returns(TransactionFeeDelegations){ option (aelf.is_view) = true; } + + rpc GetTokenAlias (google.protobuf.StringValue) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } + + rpc GetSymbolByAlias (google.protobuf.StringValue) returns (google.protobuf.StringValue) { + option (aelf.is_view) = true; + } } message TokenInfo { @@ -689,6 +705,11 @@ message GetTransactionFeeDelegateesOutput { repeated aelf.Address delegatee_addresses = 1; } +message SetSymbolAliasInput { + string symbol = 1; + string alias = 2; +} + // Events message Transferred { @@ -858,4 +879,16 @@ message TransactionFeeDelegationCancelled { aelf.Address delegator = 1 [(aelf.is_indexed) = true]; aelf.Address delegatee = 2 [(aelf.is_indexed) = true]; aelf.Address caller = 3 [(aelf.is_indexed) = true]; +} + +message SymbolAliasAdded { + option (aelf.is_event) = true; + string symbol = 1 [(aelf.is_indexed) = true]; + string alias = 2 [(aelf.is_indexed) = true]; +} + +message SymbolAliasDeleted { + option (aelf.is_event) = true; + string symbol = 1 [(aelf.is_indexed) = true]; + string alias = 2 [(aelf.is_indexed) = true]; } \ No newline at end of file diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/ACS2_TokenResourceTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/ACS2_TokenResourceTests.cs index 35f84f7154..8729acf7c1 100644 --- a/test/AElf.Contracts.MultiToken.Tests/BVT/ACS2_TokenResourceTests.cs +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/ACS2_TokenResourceTests.cs @@ -48,6 +48,24 @@ public async Task ACS2_GetResourceInfo_TransferFrom_Test() result.NonParallelizable.ShouldBeFalse(); result.WritePaths.Count.ShouldBeGreaterThan(0); } + + [Fact] + public async Task ACS2_GetResourceInfo_TransferFrom_NFT_Test() + { + var transaction = GenerateTokenTransaction(Accounts[0].Address, nameof(TokenContractStub.TransferFrom), + new TransferFromInput + { + Amount = 100, + Symbol = "ABC-1", + From = Accounts[1].Address, + To = Accounts[2].Address, + Memo = "Test get resource" + }); + + var result = await Acs2BaseStub.GetResourceInfo.CallAsync(transaction); + result.NonParallelizable.ShouldBeFalse(); + result.WritePaths.Count.ShouldBeGreaterThan(0); + } private async Task
GetDefaultParliamentAddressAsync() { @@ -179,7 +197,7 @@ await TokenContractStubDelegate3.SetTransactionFeeDelegateInfos.SendAsync(new Se var result = await Acs2BaseStub.GetResourceInfo.CallAsync(transaction); result.NonParallelizable.ShouldBeFalse(); - result.WritePaths.Count.ShouldBe(9); + result.WritePaths.Count.ShouldBe(10); } [Fact] diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenAliasTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenAliasTests.cs new file mode 100644 index 0000000000..d95e92816a --- /dev/null +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenAliasTests.cs @@ -0,0 +1,379 @@ +using System.Threading.Tasks; +using AElf.Types; +using Google.Protobuf.WellKnownTypes; +using Shouldly; +using Xunit; + +namespace AElf.Contracts.MultiToken; + +public partial class MultiTokenContractTests +{ + public const string TokenAliasExternalInfoKey = "aelf_token_alias"; + + [Fact] + public async Task SetTokenAlias_NFTCollection_Test() + { + var symbols = await CreateNftCollectionAndNft(); + await TokenContractStub.SetSymbolAlias.SendAsync(new SetSymbolAliasInput + { + Symbol = symbols[1], + Alias = "TP" + }); + + { + // Check TokenInfo of NFT Collection. + var tokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = symbols[0] + }); + tokenInfo.ExternalInfo.Value.ContainsKey(TokenAliasExternalInfoKey); + tokenInfo.ExternalInfo.Value[TokenAliasExternalInfoKey].ShouldBe("{\"TP-31175\":\"TP\"}"); + } + + { + // Check TokenInfo of NFT Item. + var tokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = "TP" + }); + tokenInfo.Symbol.ShouldBe(symbols[1]); + } + + { + // Check alias. + var alias = await TokenContractStub.GetTokenAlias.CallAsync(new StringValue { Value = "TP-31175" }); + alias.Value.ShouldBe("TP"); + } + + { + var alias = await TokenContractStub.GetSymbolByAlias.CallAsync(new StringValue { Value = "TP" }); + alias.Value.ShouldBe("TP-31175"); + } + } + + [Fact] + public async Task SetTokenAlias_NFTCollection_CollectionSymbol_Test() + { + await CreateNftCollectionAndNft(); + await TokenContractStub.SetSymbolAlias.SendAsync(new SetSymbolAliasInput + { + Symbol = "TP-0", + Alias = "TP" + }); + + { + // Check TokenInfo of NFT Collection. + var tokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = "TP-0" + }); + tokenInfo.ExternalInfo.Value.ContainsKey(TokenAliasExternalInfoKey); + tokenInfo.ExternalInfo.Value[TokenAliasExternalInfoKey].ShouldBe("{\"TP-0\":\"TP\"}"); + } + + { + // Check TokenInfo of NFT Item. + var tokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = "TP" + }); + tokenInfo.Symbol.ShouldBe("TP-0"); + } + + { + // Check alias. + var alias = await TokenContractStub.GetTokenAlias.CallAsync(new StringValue { Value = "TP-0" }); + alias.Value.ShouldBe("TP"); + } + + { + var alias = await TokenContractStub.GetSymbolByAlias.CallAsync(new StringValue { Value = "TP" }); + alias.Value.ShouldBe("TP-0"); + } + } + + [Fact] + public async Task SetTokenAlias_FT_Test() + { + await CreateNormalTokenAsync(); + + // Set token alias for FT. + var result = await TokenContractStub.SetSymbolAlias.SendWithExceptionAsync(new SetSymbolAliasInput + { + Symbol = AliceCoinTokenInfo.Symbol, + }); + result.TransactionResult.Error.ShouldContain("Token alias can only be set for NFT Item."); + } + + [Fact] + public async Task CreateTokenWithAlias_Test() + { + var createCollectionResult = await CreateNftCollectionAsync(NftCollection1155WithAliasInfo); + createCollectionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + await CreateNftAsync(NftCollection1155WithAliasInfo.Symbol, Nft721Info); + + { + // Check alias. + var alias = await TokenContractStub.GetTokenAlias.CallAsync(new StringValue { Value = "TP-31175" }); + alias.Value.ShouldBe("TP"); + } + + { + // Check TokenInfo of NFT Item. + var tokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = "TP" + }); + tokenInfo.Symbol.ShouldBe("TP-31175"); + } + } + + [Fact] + public async Task CreateTokenWithAlias_FT_Test() + { + var createInput = new CreateInput + { + Symbol = AliceCoinTokenInfo.Symbol, + TokenName = AliceCoinTokenInfo.TokenName, + TotalSupply = AliceCoinTokenInfo.TotalSupply, + Decimals = AliceCoinTokenInfo.Decimals, + Issuer = AliceCoinTokenInfo.Issuer, + Owner = AliceCoinTokenInfo.Issuer, + IsBurnable = AliceCoinTokenInfo.IsBurnable, + LockWhiteList = + { + BasicFunctionContractAddress, + OtherBasicFunctionContractAddress, + TokenConverterContractAddress, + TreasuryContractAddress + }, + ExternalInfo = new ExternalInfo + { + Value = + { + { TokenAliasExternalInfoKey, "{\"ALICE-111\":\"ALICE\"}" } + } + } + }; + await CreateSeedNftAsync(TokenContractStub, createInput); + var result = await TokenContractStub.Create.SendWithExceptionAsync(createInput); + result.TransactionResult.Error.ShouldContain("Token alias can only be set for NFT Item."); + } + + [Fact] + public async Task TransferViaAlias_Test() + { + await CreateTokenWithAlias_Test(); + + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "TP-31175", + Amount = 1, + To = DefaultAddress + }); + + { + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = DefaultAddress, + Symbol = "TP" + }); + balance.Balance.ShouldBe(1); + } + + await TokenContractStub.Transfer.SendAsync(new TransferInput + { + // Transfer via alias. + Symbol = "TP", + Amount = 1, + To = User1Address + }); + + { + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User1Address, + Symbol = "TP" + }); + balance.Balance.ShouldBe(1); + } + } + + [Fact] + public async Task ApproveAndTransferFromViaAlias_Test() + { + await CreateTokenWithAlias_Test(); + + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "TP-31175", + Amount = 1, + To = DefaultAddress + }); + + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Symbol = "TP", + Amount = 1, + Spender = User1Address + }); + + await TokenContractStubUser.TransferFrom.SendAsync(new TransferFromInput + { + Symbol = "TP", + Amount = 1, + From = DefaultAddress, + To = User2Address, + }); + + { + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User2Address, + Symbol = "TP" + }); + balance.Balance.ShouldBe(1); + } + } + + [Fact] + public async Task GetBalanceOfNotExistToken_Test() + { + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User2Address, + Symbol = "TP" + }); + balance.Balance.ShouldBe(0); + } + + [Fact] + public async Task GetAllowanceOfNotExistToken_Test() + { + var allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = User2Address, + Symbol = "TP", + Spender = DefaultAddress + }); + allowance.Allowance.ShouldBe(0); + } + + [Fact] + public async Task BatchApproveWithAlias_Test() + { + await SetTokenAlias_NFTCollection_Test(); + await CreateTokenAndIssue(); + var approveBasisResult = (await TokenContractStub.BatchApprove.SendAsync(new BatchApproveInput + { + Value = + { + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 2000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = "TP", + Amount = 1000L, + Spender = OtherBasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 5000L, + Spender = TreasuryContractAddress + } + } + })).TransactionResult; + approveBasisResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var basicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = BasicFunctionContractAddress, + Symbol = SymbolForTest + }); + basicAllowanceOutput.Allowance.ShouldBe(2000L); + var otherBasicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = OtherBasicFunctionContractAddress, + Symbol = "TP" + }); + otherBasicAllowanceOutput.Allowance.ShouldBe(1000L); + var treasuryAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = TreasuryContractAddress, + Symbol = SymbolForTest + }); + treasuryAllowanceOutput.Allowance.ShouldBe(5000L); + + approveBasisResult = (await TokenContractStub.BatchApprove.SendAsync(new BatchApproveInput + { + Value = + { + new ApproveInput + { + Symbol = "TP", + Amount = 1000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 3000L, + Spender = BasicFunctionContractAddress + }, + new ApproveInput + { + Symbol = SymbolForTest, + Amount = 3000L, + Spender = TreasuryContractAddress + } + } + })).TransactionResult; + approveBasisResult.Status.ShouldBe(TransactionResultStatus.Mined); + basicAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = BasicFunctionContractAddress, + Symbol = SymbolForTest + }); + basicAllowanceOutput.Allowance.ShouldBe(3000L); + + treasuryAllowanceOutput = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = TreasuryContractAddress, + Symbol = SymbolForTest + }); + treasuryAllowanceOutput.Allowance.ShouldBe(3000L); + } + + private TokenInfo NftCollection1155WithAliasInfo => new() + { + Symbol = "TP-", + TokenName = "Trump Digital Trading Cards #1155", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = DefaultAddress, + IssueChainId = _chainId, + ExternalInfo = new ExternalInfo + { + Value = + { + { + NftCollectionMetaFields.ImageUrlKey, + "https://i.seadn.io/gcs/files/0f5cdfaaf687de2ebb5834b129a5bef3.png?auto=format&w=3840" + }, + { NftCollectionMetaFields.NftType, NftType }, + { TokenAliasExternalInfoKey, "{\"TP-31175\":\"TP\"}" } + } + } + }; +} \ No newline at end of file diff --git a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs index 49642bae3a..201e56d6e6 100644 --- a/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs +++ b/test/AElf.Contracts.MultiToken.Tests/BVT/TokenApplicationTests.cs @@ -504,6 +504,452 @@ await TreasuryContractStub.Donate.SendAsync(new DonateInput afterTransferFromBalance.Balance.ShouldBe(beforeTransferFromBalance.Balance.Sub(transferAmount)); } + private async Task CreateNft() + { + await CreateMutiTokenAsync(TokenContractStub, new CreateInput + { + TokenName = "Test", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = DefaultAddress, + Owner = DefaultAddress, + IssueChainId = _chainId, + Symbol = "ABC-0" + }); + await TokenContractStub.Create.SendAsync(new CreateInput + { + TokenName = "Test", + TotalSupply = TotalSupply, + Decimals = 0, + Issuer = DefaultAddress, + Owner = DefaultAddress, + IssueChainId = _chainId, + Symbol = "ABC-1" + }); + } + [Fact] + public async Task MultiTokenContract_TransferFrom_Nft_Global_Test() + { + await CreateNft(); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "ABC-1", + Amount = 100, + To = DefaultAddress, + Memo = "test" + }); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "ABC-1", + Amount = 200, + To = User1Address, + Memo = "test" + }); + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = DefaultAddress, + Symbol = "ABC-1" + }); + balance.Balance.ShouldBe(100); + balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User1Address, + Symbol = "ABC-1" + }); + balance.Balance.ShouldBe(200); + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Amount = 1000, + Symbol = "*", + Spender = User1Address + }); + + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Amount = 1, + Symbol = "ABC-*", + Spender = User1Address + }); + var allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + allowance.Allowance.ShouldBe(0); + allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ELF" + }); + allowance.Allowance.ShouldBe(0); + { + var realAllowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + realAllowance.Allowance.ShouldBe(1000); + } + { + var realAllowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ELF" + }); + realAllowance.Allowance.ShouldBe(1000); + } + var user1Stub = + GetTester(TokenContractAddress, User1KeyPair); + var result2 = await user1Stub.TransferFrom.SendAsync(new TransferFromInput + { + Amount = 50, + From = DefaultAddress, + Memo = "test", + Symbol = "ABC-1", + To = User1Address + }); + result2.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + { + var realAllowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + realAllowance.Allowance.ShouldBe(0); + } + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + allowance.Allowance.ShouldBe(1000-50); + balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = DefaultAddress, + Symbol = "ABC-1" + }); + balance.Balance.ShouldBe(50); + balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User1Address, + Symbol = "ABC-1" + }); + balance.Balance.ShouldBe(250); + } + + [Fact] + public async Task MultiTokenContract_TransferFrom_Nft_Collection_Test() + { + await CreateNft(); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "ABC-1", + Amount = 100, + To = DefaultAddress, + Memo = "test" + }); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "ABC-1", + Amount = 200, + To = User1Address, + Memo = "test" + }); + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Amount = 20, + Symbol = "*", + Spender = User1Address + }); + + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Amount = 1000, + Symbol = "ABC-*", + Spender = User1Address + }); + { + var realAllowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + realAllowance.Allowance.ShouldBe(0); + } + var allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + allowance.Allowance.ShouldBe(1000); + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ELF" + }); + allowance.Allowance.ShouldBe(20); + var user1Stub = + GetTester(TokenContractAddress, User1KeyPair); + var result2 = await user1Stub.TransferFrom.SendAsync(new TransferFromInput + { + Amount = 50, + From = DefaultAddress, + Memo = "test", + Symbol = "ABC-1", + To = User1Address + }); + result2.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ABC-1" + }); + allowance.Allowance.ShouldBe(1000-50); + allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "*" + }); + allowance.Allowance.ShouldBe(20); + + } + + [Fact] + public async Task MultiTokenContract_TransferFrom_Token_Test() + { + await CreateAndIssueToken(); + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Amount = 100_00000000, + Symbol = "*", + Spender = User1Address + }); + var allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + allowance.Allowance.ShouldBe(0); + { + var realAllowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + realAllowance.Allowance.ShouldBe(100_00000000); + } + allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "ELF" + }); + allowance.Allowance.ShouldBe(0); + { + var realAllowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + realAllowance.Allowance.ShouldBe(100_00000000); + } + var user1Stub = + GetTester(TokenContractAddress, User1KeyPair); + var result2 = await user1Stub.TransferFrom.SendAsync(new TransferFromInput + { + Amount = 50_00000000, + From = DefaultAddress, + Memo = "test", + Symbol = "SSS", + To = User1Address + }); + result2.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + allowance.Allowance.ShouldBe(100_00000000-50_00000000); + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = DefaultAddress, + Symbol = "SSS" + }); + balance.Balance.ShouldBe(TotalSupply - 50_00000000); + balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = User1Address, + Symbol = "SSS" + }); + balance.Balance.ShouldBe(50_00000000); + } + + private async Task CreateAndIssueToken() + { + await CreateMutiTokenAsync(TokenContractStub, new CreateInput + { + TokenName = "Test", + TotalSupply = TotalSupply, + Decimals = 8, + Issuer = DefaultAddress, + Owner = DefaultAddress, + IssueChainId = _chainId, + Symbol = "SSS" + }); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "SSS", + Amount = TotalSupply, + To = DefaultAddress, + Memo = "Issue" + }); + var balance = await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = DefaultAddress, + Symbol = "SSS" + }); + balance.Balance.ShouldBe(TotalSupply); + } + [Fact] + public async Task MultiTokenContract_Approve_Test_New() + { + await CreateAndIssueToken(); + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "SSS", + Amount = 100_000000000 + }); + var allowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + allowance.Allowance.ShouldBe(100_000000000); + await TokenContractStub.Approve.SendAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "*", + Amount = 200_000000000 + }); + { + var realAllowance = await TokenContractStub.GetAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + realAllowance.Allowance.ShouldBe(100_000000000); + } + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "SSS" + }); + allowance.Allowance.ShouldBe(200_000000000); + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "*" + }); + allowance.Allowance.ShouldBe(200_000000000); + await TokenContractStub.UnApprove.SendAsync(new UnApproveInput + { + Spender = User1Address, + Symbol = "*", + Amount = 20_000000000 + }); + allowance = await TokenContractStub.GetAvailableAllowance.CallAsync(new GetAllowanceInput + { + Owner = DefaultAddress, + Spender = User1Address, + Symbol = "*" + }); + allowance.Allowance.ShouldBe(200_000000000-20_000000000); + } + + [Fact] + public async Task MultiTokenContract_Approve_Test_New_Fail() + { + await CreateAndIssueToken(); + { + var executionResult = await TokenContractStub.Approve.SendWithExceptionAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "SSS*", + Amount = 100_000000000 + }); + executionResult.TransactionResult.Error.ShouldContain("Invalid symbol."); + } + { + var executionResult = await TokenContractStub.Approve.SendWithExceptionAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "SSS**", + Amount = 100_000000000 + }); + executionResult.TransactionResult.Error.ShouldContain("Invalid symbol."); + } + { + var executionResult = await TokenContractStub.Approve.SendWithExceptionAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "*-*", + Amount = 100_000000000 + }); + executionResult.TransactionResult.Error.ShouldContain("Token is not found"); + } + } + + [Fact] + public async Task MultiTokenContract_Approve_Test_New_Nft_Fail() + { + await CreateNft(); + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = "ABC-1", + Amount = 100, + To = DefaultAddress, + Memo = "test" + }); + { + var executionResult = await TokenContractStub.Approve.SendWithExceptionAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "AB*-*", + Amount = 100_000000000 + }); + executionResult.TransactionResult.Error.ShouldContain("Invalid Symbol"); + } + { + var executionResult = await TokenContractStub.Approve.SendWithExceptionAsync(new ApproveInput + { + Spender = User1Address, + Symbol = "ABC-*9", + Amount = 100_000000000 + }); + executionResult.TransactionResult.Error.ShouldContain("Invalid NFT Symbol."); + } + } + private async Task CreateTokenAndIssue(List
whitelist = null, Address issueTo = null) { if (whitelist == null) diff --git a/test/AElf.Contracts.MultiTokenCrossChainTransfer.Tests/MultiTokenContractCrossChainTest.cs b/test/AElf.Contracts.MultiTokenCrossChainTransfer.Tests/MultiTokenContractCrossChainTest.cs index 4c76a04b09..30a6d59c74 100644 --- a/test/AElf.Contracts.MultiTokenCrossChainTransfer.Tests/MultiTokenContractCrossChainTest.cs +++ b/test/AElf.Contracts.MultiTokenCrossChainTransfer.Tests/MultiTokenContractCrossChainTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using AElf.Contracts.Parliament; using AElf.ContractTestBase.ContractTestKit; @@ -11,6 +12,7 @@ using AElf.Standards.ACS7; using AElf.Types; using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; using Shouldly; using Xunit; @@ -19,11 +21,13 @@ namespace AElf.Contracts.MultiToken; public class MultiTokenContractCrossChainTest : MultiTokenContractCrossChainTestBase { private const string SymbolForTesting = "ELFTEST"; + private const string NFTSymbolForTesting = "ELFNFT"; private const string NativeToken = "ELF"; private static readonly long _totalSupply = 1000L; private readonly Hash _fakeBlockHeader = HashHelper.ComputeFrom("fakeBlockHeader"); private readonly int _parentChainHeightOfCreation = 5; private readonly string sideChainSymbol = "STA"; + public const string TokenAliasExternalInfoKey = "aelf_token_alias"; #region register test @@ -321,7 +325,7 @@ public async Task SideChain_CrossChainSideChainCreateToken_Test() } - [Fact] + [Fact(Skip = "Now we allow this.")] public async Task SideChain_CrossChainCreateToken_WithAlreadyCreated_Test() { await GenerateSideChainAsync(); @@ -387,6 +391,146 @@ public async Task CrossChainCreateToken_With_Invalid_Verification_Test() Assert.True(result.Status == TransactionResultStatus.Failed); Assert.Contains("Invalid transaction", result.Error); } + + [Fact] + public async Task SideChain_CrossChainSideChainCreateToken_WithAlias_Test() + { + await GenerateSideChainAsync(); + await RegisterSideChainContractAddressOnMainChainAsync(); + + // Main chain create token + await BootMinerChangeRoundAsync(AEDPoSContractStub, true); + var createTransaction = await CreateTransactionForNFTCreation(TokenContractStub, + DefaultAccount.Address, $"{NFTSymbolForTesting}-0", TokenContractAddress); + var blockExecutedSet = await MineAsync(new List { createTransaction }); + var createResult = blockExecutedSet.TransactionResultMap[createTransaction.GetHash()]; + Assert.True(createResult.Status == TransactionResultStatus.Mined, createResult.Error); + + var createdTokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = $"{NFTSymbolForTesting}-0" + }); + var tokenValidationTransaction = CreateTokenInfoValidationTransaction(createdTokenInfo, + TokenContractStub); + + blockExecutedSet = await MineAsync(new List { tokenValidationTransaction }); + var merklePath = GetTransactionMerklePathAndRoot(tokenValidationTransaction, out var blockRoot); + await IndexMainChainTransactionAsync(blockExecutedSet.Height, blockRoot, blockRoot); + var crossChainCreateTokenInput = new CrossChainCreateTokenInput + { + FromChainId = MainChainId, + ParentChainHeight = blockExecutedSet.Height, + TransactionBytes = tokenValidationTransaction.ToByteString(), + MerklePath = merklePath + }; + // Side chain cross chain create + var executionResult = + await SideChainTokenContractStub.CrossChainCreateToken.SendAsync(crossChainCreateTokenInput); + executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined, + executionResult.TransactionResult.Error); + + var newTokenInfo = await SideChainTokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = $"{NFTSymbolForTesting}-0" + }); + newTokenInfo.TotalSupply.ShouldBe(_totalSupply); + + var alias = await SideChainTokenContractStub.GetTokenAlias.CallAsync(new StringValue + { + Value = $"{NFTSymbolForTesting}-{1}" + }); + alias.Value.ShouldBe(NFTSymbolForTesting); + } + + [Fact] + public async Task SideChain_CrossChainSideChainCreateToken_SetAliasAndSyncAgain_Test() + { + await GenerateSideChainAsync(); + await RegisterSideChainContractAddressOnMainChainAsync(); + + // Main chain create token + await BootMinerChangeRoundAsync(AEDPoSContractStub, true); + var createTransaction = await CreateTransactionForNFTCreation(TokenContractStub, + DefaultAccount.Address, $"{NFTSymbolForTesting}-0", TokenContractAddress, false); + var blockExecutedSet = await MineAsync(new List { createTransaction }); + var createResult = blockExecutedSet.TransactionResultMap[createTransaction.GetHash()]; + Assert.True(createResult.Status == TransactionResultStatus.Mined, createResult.Error); + + // Sync for the first time + { + var createdTokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = $"{NFTSymbolForTesting}-0" + }); + var tokenValidationTransaction = CreateTokenInfoValidationTransaction(createdTokenInfo, + TokenContractStub); + + blockExecutedSet = await MineAsync(new List { tokenValidationTransaction }); + var merklePath = GetTransactionMerklePathAndRoot(tokenValidationTransaction, out var blockRoot); + await IndexMainChainTransactionAsync(blockExecutedSet.Height, blockRoot, blockRoot); + var crossChainCreateTokenInput = new CrossChainCreateTokenInput + { + FromChainId = MainChainId, + ParentChainHeight = blockExecutedSet.Height, + TransactionBytes = tokenValidationTransaction.ToByteString(), + MerklePath = merklePath + }; + // Side chain cross chain create + var executionResult = + await SideChainTokenContractStub.CrossChainCreateToken.SendAsync(crossChainCreateTokenInput); + executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined, + executionResult.TransactionResult.Error); + } + + // Set Alias + { + var setAliasTransaction = await TokenContractStub.SetSymbolAlias.SendAsync(new SetSymbolAliasInput + { + Symbol = $"{NFTSymbolForTesting}-1", + Alias = NFTSymbolForTesting + }); + var setAliasResult = setAliasTransaction.TransactionResult; + setAliasResult.Status.ShouldBe(TransactionResultStatus.Mined); + } + + // Sync for the second time + { + var createdTokenInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = $"{NFTSymbolForTesting}-0" + }); + var tokenValidationTransaction = CreateTokenInfoValidationTransaction(createdTokenInfo, + TokenContractStub); + + blockExecutedSet = await MineAsync(new List { tokenValidationTransaction }); + var merklePath = GetTransactionMerklePathAndRoot(tokenValidationTransaction, out var blockRoot); + await IndexMainChainTransactionAsync(blockExecutedSet.Height, blockRoot, blockRoot); + var crossChainCreateTokenInput = new CrossChainCreateTokenInput + { + FromChainId = MainChainId, + ParentChainHeight = blockExecutedSet.Height, + TransactionBytes = tokenValidationTransaction.ToByteString(), + MerklePath = merklePath + }; + // Side chain cross chain create + var executionResult = + await SideChainTokenContractStub.CrossChainCreateToken.SendAsync(crossChainCreateTokenInput); + executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined, + executionResult.TransactionResult.Error); + } + + var newTokenInfo = await SideChainTokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = $"{NFTSymbolForTesting}-0" + }); + newTokenInfo.TotalSupply.ShouldBe(_totalSupply); + + var alias = await SideChainTokenContractStub.GetTokenAlias.CallAsync(new StringValue + { + Value = $"{NFTSymbolForTesting}-1" + }); + alias.Value.ShouldBe(NFTSymbolForTesting); + } #endregion @@ -891,12 +1035,48 @@ private async Task CreateTransactionForTokenCreation( await CreateSeedNftAsync(tokenContractImplStub, input, lockWhiteAddress); return tokenContractImplStub.Create.GetTransaction(input); } + + private async Task CreateTransactionForNFTCreation( + TokenContractImplContainer.TokenContractImplStub tokenContractImplStub, + Address issuer, string symbol, Address lockWhiteAddress, bool withAlias = true) + { + await CreateSeedNftCollection(tokenContractImplStub, issuer); + var tokenInfo = GetTokenInfo(symbol, issuer); + var input = new CreateInput + { + Symbol = tokenInfo.Symbol, + Decimals = 0, + Issuer = tokenInfo.Issuer, + Owner = tokenInfo.Issuer, + IsBurnable = tokenInfo.IsBurnable, + TokenName = tokenInfo.TokenName, + TotalSupply = tokenInfo.TotalSupply, + }; + if (withAlias) + { + input.ExternalInfo = new ExternalInfo + { + Value = + { + { + TokenAliasExternalInfoKey, JsonSerializer.Serialize(new Dictionary + { + { $"{NFTSymbolForTesting}-{1}", NFTSymbolForTesting } + }) + } + } + }; + } + + await CreateSeedNftAsync(tokenContractImplStub, input, lockWhiteAddress); + return tokenContractImplStub.Create.GetTransaction(input); + } private Transaction CreateTokenInfoValidationTransaction(TokenInfo createdTokenInfo, TokenContractImplContainer.TokenContractImplStub tokenContractImplStub) { - return tokenContractImplStub.ValidateTokenInfoExists.GetTransaction(new ValidateTokenInfoExistsInput + var input = new ValidateTokenInfoExistsInput { TokenName = createdTokenInfo.TokenName, Symbol = createdTokenInfo.Symbol, @@ -906,7 +1086,12 @@ private Transaction CreateTokenInfoValidationTransaction(TokenInfo createdTokenI IsBurnable = createdTokenInfo.IsBurnable, TotalSupply = createdTokenInfo.TotalSupply, IssueChainId = createdTokenInfo.IssueChainId - }); + }; + if (createdTokenInfo.ExternalInfo != null) + { + input.ExternalInfo.Add(createdTokenInfo.ExternalInfo.Value); + } + return tokenContractImplStub.ValidateTokenInfoExists.GetTransaction(input); } private TokenInfo GetTokenInfo(string symbol, Address issuer, bool isBurnable = true)