diff --git a/.gitignore b/.gitignore index 731df3ce..b4b59192 100644 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,6 @@ paket-files/ __pycache__/ *.pyc Testnet/AddressMapper/readme.ps1 + +# macOS +.DS_Store \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/AddressExtensions.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/AddressExtensions.cs new file mode 100644 index 00000000..fb696204 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/AddressExtensions.cs @@ -0,0 +1,40 @@ +using Stratis.SmartContracts; +using System; + +namespace NonFungibleTicketContract.Tests +{ + public static class AddressExtensions + { + private static byte[] HexStringToBytes(string val) + { + if (val.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + val = val.Substring(2); + + byte[] ret = new byte[val.Length / 2]; + for (int i = 0; i < val.Length; i = i + 2) + { + string hexChars = val.Substring(i, 2); + ret[i / 2] = byte.Parse(hexChars, System.Globalization.NumberStyles.HexNumber); + } + return ret; + } + + public static Address HexToAddress(this string hexString) + { + // uint160 only parses a big-endian hex string + var result = HexStringToBytes(hexString); + return CreateAddress(result); + } + + private static Address CreateAddress(byte[] bytes) + { + uint pn0 = BitConverter.ToUInt32(bytes, 0); + uint pn1 = BitConverter.ToUInt32(bytes, 4); + uint pn2 = BitConverter.ToUInt32(bytes, 8); + uint pn3 = BitConverter.ToUInt32(bytes, 12); + uint pn4 = BitConverter.ToUInt32(bytes, 16); + + return new Address(pn0, pn1, pn2, pn3, pn4); + } + } +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/InMemoryState.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/InMemoryState.cs new file mode 100644 index 00000000..6096af96 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/InMemoryState.cs @@ -0,0 +1,85 @@ +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; + +namespace NonFungibleTicketContract.Tests +{ + public class InMemoryState : IPersistentState + { + private readonly Dictionary storage = new Dictionary(); + public bool IsContractResult { get; set; } + public void Clear(string key) => storage.Remove(key); + + public bool ContainsKey(string key) => storage.ContainsKey(key); + + public T GetValue(string key) => (T)storage.GetValueOrDefault(key, default(T)); + + public void AddOrReplace(string key, object value) + { + if (!storage.TryAdd(key, value)) + storage[key] = value; + } + public Address GetAddress(string key) => GetValue
(key); + + public T[] GetArray(string key) => GetValue(key) ?? Array.Empty(); + + public bool GetBool(string key) => GetValue(key); + + public byte[] GetBytes(byte[] key) => throw new NotImplementedException(); + + public byte[] GetBytes(string key) => GetValue(key); + + public char GetChar(string key) => GetValue(key); + + public int GetInt32(string key) => GetValue(key); + + public long GetInt64(string key) => GetValue(key); + + public string GetString(string key) => GetValue(key); + + public T GetStruct(string key) + where T : struct => GetValue(key); + + public uint GetUInt32(string key) => GetValue(key); + + public ulong GetUInt64(string key) => GetValue(key); + + public UInt128 GetUInt128(string key) => GetValue(key); + + public UInt256 GetUInt256(string key) => GetValue(key); + + public bool IsContract(Address address) => IsContractResult; + + public void SetAddress(string key, Address value) => AddOrReplace(key, value); + + public void SetArray(string key, Array a) => AddOrReplace(key, a); + + public void SetBool(string key, bool value) => AddOrReplace(key, value); + + public void SetBytes(byte[] key, byte[] value) + { + throw new NotImplementedException(); + } + + public void SetBytes(string key, byte[] value) => AddOrReplace(key, value); + + public void SetChar(string key, char value) => AddOrReplace(key, value); + + public void SetInt32(string key, int value) => AddOrReplace(key, value); + + public void SetInt64(string key, long value) => AddOrReplace(key, value); + + public void SetString(string key, string value) => AddOrReplace(key, value); + + public void SetStruct(string key, T value) + where T : struct => AddOrReplace(key, value); + + public void SetUInt32(string key, uint value) => AddOrReplace(key, value); + + public void SetUInt64(string key, ulong value) => AddOrReplace(key, value); + + public void SetUInt128(string key, UInt128 value) => AddOrReplace(key, value); + + public void SetUInt256(string key, UInt256 value) => AddOrReplace(key, value); + } +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicket.Tests.csproj b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicket.Tests.csproj new file mode 100644 index 00000000..94754915 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicket.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicketTests.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicketTests.cs new file mode 100644 index 00000000..4c4f9367 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTicketTests.cs @@ -0,0 +1,248 @@ +using System; +using FluentAssertions; +using Moq; +using Stratis.SmartContracts; +using Xunit; + +namespace NonFungibleTicketContract.Tests +{ + public class NonFungibleTicketTests + { + private readonly Mock smartContractStateMock; + private readonly Mock contractLoggerMock; + private readonly Mock message; + private readonly Mock transactionExecutorMock; + private InMemoryState state; + private Address owner; + private string name; + private string symbol; + + public NonFungibleTicketTests() + { + this.contractLoggerMock = new Mock(); + this.smartContractStateMock = new Mock(); + this.transactionExecutorMock = new Mock(); + this.message = new Mock(); + this.state = new InMemoryState(); + this.smartContractStateMock.Setup(s => s.PersistentState).Returns(this.state); + this.smartContractStateMock.Setup(s => s.ContractLogger).Returns(this.contractLoggerMock.Object); + this.smartContractStateMock.Setup(s => s.InternalTransactionExecutor).Returns(this.transactionExecutorMock.Object); + this.smartContractStateMock.Setup(s => s.Message).Returns(this.message.Object); + this.owner = "0x0000000000000000000000000000000000000005".HexToAddress(); + this.name = "Non-Fungible Ticket"; + this.symbol = "NFT"; + smartContractStateMock.Setup(m => m.Message.Sender).Returns(owner); + } + + [Fact] + public void Constructor_Sets_Values() + { + CreateNonFungibleTicket(); + Assert.True(state.GetBool($"SupportedInterface:{(int)TokenInterface.IRedeemableTicketPerks}")); + Assert.True(state.GetBool($"SupportedInterface:{(int)TokenInterface.IAuthorizableRedemptions}")); + Assert.True(state.GetBool("OwnerOnlyMinting")); + } + + [Fact] + public void GetRedemptions_TokenNotMinted_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.Invoking(token => token.GetRedemptions(5)) + .Should() + .ThrowExactly() + .WithMessage("Token id does not exist."); + } + + [Fact] + public void GetRedemptions_TokenMintedButNothingRedeemed_DefaultValue() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.GetRedemptions(1).Should().BeEquivalentTo(new bool[256]); + } + + [Fact] + public void GetRedemptions_SomePerksRedeemed_ReturnRedeemedPerks() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.AssignRedeemRole(owner); + nft.RedeemPerks(1, new byte[] { 1, 4, 8 }); + var redeemedPerks = new bool[256]; + redeemedPerks[1] = true; + redeemedPerks[4] = true; + redeemedPerks[8] = true; + nft.GetRedemptions(1).Should().BeEquivalentTo(redeemedPerks); + } + + [Fact] + public void IsRedeemed_TokenNotMinted_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.Invoking(token => token.IsRedeemed(5, 0)) + .Should() + .ThrowExactly() + .WithMessage("Token id does not exist."); + } + + [Fact] + public void IsRedeemed_TokenMintedButNotRedeemed_DefaultValue() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.IsRedeemed(1, 5).Should().Be(false); + } + + [Fact] + public void IsRedeemed_TokenRedeemed_ReturnTrue() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.AssignRedeemRole(owner); + nft.RedeemPerks(1, new byte[] { 5 }); + nft.IsRedeemed(1, 5).Should().Be(true); + } + + [Fact] + public void RedeemPerks_TokenNotMinted_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.AssignRedeemRole(owner); + nft.Invoking(token => token.RedeemPerks(5, new byte[] { 0 })) + .Should() + .ThrowExactly() + .WithMessage("Token id does not exist."); + } + + [Fact] + public void RedeemPerks_NotAssignedRedeemRole_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.Invoking(token => token.RedeemPerks(5, new byte[] { 0 })) + .Should() + .ThrowExactly() + .WithMessage("Only assigned addresses can redeem perks."); + } + + [Fact] + public void RedeemPerks_NoPerksToRedeem_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.AssignRedeemRole(owner); + nft.Invoking(token => token.RedeemPerks(1, Array.Empty())) + .Should() + .ThrowExactly() + .WithMessage("Must provide at least one perk to redeem."); + } + + [Fact] + public void RedeemPerks_Success_SetState() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Mint("0x0000000000000000000000000000000000000005".HexToAddress(), "https://nft.data/1"); + nft.AssignRedeemRole(owner); + nft.RedeemPerks(1, new byte[] { 0 }); + state.GetArray("Redemptions:1")[0].Should().Be(true); + } + + [Fact] + public void AssignRedeemRole_NotOwner_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns("0x0000000024000000000300000000000000000005".HexToAddress()); + nft.Invoking(token => token.AssignRedeemRole("0x0000000024000000000300000000000000000006".HexToAddress())) + .Should() + .ThrowExactly() + .WithMessage("The method is owner only."); + } + + [Fact] + public void AssignRedeemRole_AlreadyAssigned_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.AssignRedeemRole("0x0000000024000000000300000000000000000006".HexToAddress()); + nft.Invoking(token => token.AssignRedeemRole("0x0000000024000000000300000000000000000006".HexToAddress())) + .Should() + .ThrowExactly() + .WithMessage("Redeem role is already assigned to this address."); + } + + [Fact] + public void AssignRedeemRole_Success_SetState() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + var address = "0x0000000024000000000300000000000000000006".HexToAddress(); + nft.AssignRedeemRole(address); + state.GetBool($"Redeemer:{address}").Should().Be(true); + } + + [Fact] + public void RevokeRedeemRole_NotOwner_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns("0x0000000024000000000300000000000000000005".HexToAddress()); + nft.Invoking(token => token.RevokeRedeemRole("0x0000000024000000000300000000000000000006".HexToAddress())) + .Should() + .ThrowExactly() + .WithMessage("The method is owner only."); + } + + [Fact] + public void RevokeRedeemRole_NotAssigned_AssertFailure() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.Invoking(token => token.RevokeRedeemRole("0x0000000024000000000300000000000000000006".HexToAddress())) + .Should() + .ThrowExactly() + .WithMessage("Redeem role is not assigned to this address."); + } + + [Fact] + public void RevokeRedeemRole_Success_SetState() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + var address = "0x0000000024000000000300000000000000000006".HexToAddress(); + nft.AssignRedeemRole(address); + nft.RevokeRedeemRole(address); + state.GetBool($"Redeemer:{address}").Should().Be(false); + } + + [Fact] + public void CanRedeemPerks_NotAssignedRedeemRole_ReturnFalse() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.CanRedeemPerks(owner).Should().Be(false); + } + + [Fact] + public void CanRedeemPerks_AssignedRedeemRole_ReturnTrue() + { + var nft = CreateNonFungibleTicket(); + message.SetupGet(callTo => callTo.Sender).Returns(owner); + nft.AssignRedeemRole(owner); + nft.CanRedeemPerks(owner).Should().Be(true); + } + + private NonFungibleTicket CreateNonFungibleTicket() => new NonFungibleTicket(smartContractStateMock.Object, name, symbol); + } +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTokenTests.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTokenTests.cs new file mode 100644 index 00000000..25408ba6 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/NonFungibleTokenTests.cs @@ -0,0 +1,1208 @@ +using FluentAssertions; +using Moq; +using Moq.Language.Flow; +using Stratis.SmartContracts; +using System; +using NonFungibleTicketContract.Tests; +using Xunit; +using static NonFungibleToken; + +namespace NonFungibleTokenContract.Tests +{ + public class NonFungibleTokenTests + { + private Mock smartContractStateMock; + private Mock contractLoggerMock; + private InMemoryState state; + private Mock transactionExecutorMock; + private Address contractAddress; + private string name; + private string symbol; + private bool ownerOnlyMinting; + + public NonFungibleTokenTests() + { + this.contractLoggerMock = new Mock(); + this.smartContractStateMock = new Mock(); + this.transactionExecutorMock = new Mock(); + this.state = new InMemoryState(); + this.smartContractStateMock.Setup(s => s.PersistentState).Returns(this.state); + this.smartContractStateMock.Setup(s => s.ContractLogger).Returns(this.contractLoggerMock.Object); + this.smartContractStateMock.Setup(x => x.InternalTransactionExecutor).Returns(this.transactionExecutorMock.Object); + this.contractAddress = "0x0000000000000000000000000000000000000001".HexToAddress(); + this.name = "Non-Fungible Token"; + this.symbol = "NFT"; + this.ownerOnlyMinting = true; + } + + public string GetTokenURI(UInt256 tokenId) => $"https://example.com/api/tokens/{tokenId}"; + + [Fact] + public void Constructor_Sets_Values() + { + var owner = "0x0000000000000000000000000000000000000005".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(owner); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.True(state.GetBool("SupportedInterface:1")); + Assert.True(state.GetBool("SupportedInterface:2")); + Assert.False(state.GetBool("SupportedInterface:3")); + Assert.True(state.GetBool("SupportedInterface:4")); + Assert.False(state.GetBool("SupportedInterface:5")); + Assert.Equal(name, nonFungibleToken.Name); + Assert.Equal(symbol, nonFungibleToken.Symbol); + Assert.Equal(owner, nonFungibleToken.Owner); + Assert.Equal(ownerOnlyMinting, state.GetBool("OwnerOnlyMinting")); + } + + [Fact] + public void SetPendingOwner_Success() + { + var owner = "0x0000000000000000000000000000000000000002".HexToAddress(); + var newOwner = "0x0000000000000000000000000000000000000003".HexToAddress(); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, owner, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SetPendingOwner(newOwner); + + nonFungibleToken.Owner + .Should() + .Be(owner); + + nonFungibleToken.PendingOwner + .Should() + .Be(newOwner); + + var log = new OwnershipTransferRequestedLog { CurrentOwner = owner, PendingOwner = newOwner }; + contractLoggerMock.Verify(l => l.Log(It.IsAny(), log)); + } + + [Fact] + public void SetPendingOwner_Called_By_NonOwner_Fails() + { + var owner = "0x0000000000000000000000000000000000000002".HexToAddress(); + var newOwner = "0x0000000000000000000000000000000000000003".HexToAddress(); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, owner, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, newOwner, 0)); + + nonFungibleToken.Invoking(c => c.SetPendingOwner(newOwner)) + .Should() + .ThrowExactly() + .WithMessage("The method is owner only."); + } + + [Fact] + public void ClaimOwnership_Not_Called_By_NewOwner_Fails() + { + var owner = "0x0000000000000000000000000000000000000002".HexToAddress(); + var newOwner = "0x0000000000000000000000000000000000000003".HexToAddress(); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, owner, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SetPendingOwner(newOwner); + + nonFungibleToken.Invoking(c => c.ClaimOwnership()) + .Should() + .ThrowExactly() + .WithMessage("ClaimOwnership must be called by the new(pending) owner."); + } + + [Fact] + public void ClaimOwnership_Success() + { + var owner = "0x0000000000000000000000000000000000000002".HexToAddress(); + var newOwner = "0x0000000000000000000000000000000000000003".HexToAddress(); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, owner, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SetPendingOwner(newOwner); + + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, newOwner, 0)); + + nonFungibleToken.ClaimOwnership(); + + nonFungibleToken.Owner + .Should() + .Be(newOwner); + + nonFungibleToken.PendingOwner + .Should() + .Be(Address.Zero); + + var log = new OwnershipTransferredLog { PreviousOwner = owner, NewOwner = newOwner }; + contractLoggerMock.Verify(l => l.Log(It.IsAny(), log)); + } + + [Fact] + public void SupportsInterface_InterfaceSupported_ReturnsTrue() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.SupportsInterface(2); + + Assert.True(result); + } + + [Fact] + public void SupportsInterface_InterfaceSetToFalseSupported_ReturnsFalse() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + state.SetBool("SupportedInterface:2", false); + + var result = nonFungibleToken.SupportsInterface(3); + + Assert.False(result); + } + + [Fact] + public void SupportsInterface_InterfaceNotSupported_ReturnsFalse() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.SupportsInterface(6); + + Assert.False(result); + } + + + [Fact] + public void GetApproved_NotValidNFToken_OwnerAddressZero_ThrowsException() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.GetApproved(1)); + } + + [Fact] + public void GetApproved_ApprovalNotInStorage_ReturnsZeroAddress() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + state.SetAddress("IdToOwner:1", "0x0000000000000000000000000000000000000005".HexToAddress()); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.GetApproved(1); + + Assert.Equal(Address.Zero, result); + } + + [Fact] + public void GetApproved_ApprovalInStorage_ReturnsAddress() + { + + var approvalAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", "0x0000000000000000000000000000000000000005".HexToAddress()); + state.SetAddress("IdToApproval:1", approvalAddress); + + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + var result = nonFungibleToken.GetApproved(1); + + Assert.Equal(approvalAddress, result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorInStateAsTrue_ReturnsTrue() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddresss}", true); + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.True(result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorInStateAsFalse_ReturnsFalse() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddresss}", false); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.False(result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorNotInState_ReturnsFalse() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.False(result); + } + + [Fact] + public void OwnerOf_IdToOwnerNotInStorage_ThrowsException() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.OwnerOf(1)); + } + + [Fact] + public void OwnerOf_NFTokenMappedToAddressZero_ThrowsException() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + state.SetAddress("IdToOwner:1", Address.Zero); + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.OwnerOf(1)); + } + + [Fact] + public void OwnerOf_NFTokenExistsWithOwner_ReturnsOwnerAddress() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.OwnerOf(1); + + Assert.Equal(ownerAddress, result); + } + + [Fact] + public void BalanceOf_OwnerZero_ThrowsException() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => { nonFungibleToken.BalanceOf(Address.Zero); }); + } + + [Fact] + public void BalanceOf_NftTokenCountNotInStorage_ReturnsZero() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.BalanceOf(ownerAddress); + + Assert.Equal(0, result); + } + + [Fact] + public void BalanceOf_OwnerNftTokenCountInStorage_ReturnsTokenCount() + { + var sender = "0x0000000000000000000000000000000000000002".HexToAddress(); + smartContractStateMock.SetupGet(m => m.Message).Returns(new Message(contractAddress, sender, 0)); + + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetUInt256($"Balance:{ownerAddress}", 15); + var nonFungibleToken = CreateNonFungibleToken(); + + var result = nonFungibleToken.BalanceOf(ownerAddress); + + Assert.Equal(15, result); + } + + [Fact] + public void SetApprovalForAll_SetsMessageSender_ToOperatorApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SetApprovalForAll(operatorAddress, true); + + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalForAllLog { Owner = ownerAddress, Operator = operatorAddress, Approved = true })); + } + + [Fact] + public void Approve_TokenOwnerNotMessageSenderOrOperator_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(someAddress, 1)); + } + + [Fact] + public void Approve_ValidApproval_SwitchesOwnerToApprovedForNFToken() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.Approve(someAddress, 1); + + Assert.Equal(state.GetAddress("IdToApproval:1"), someAddress); + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalLog { Owner = ownerAddress, Approved = someAddress, TokenId = 1 })); + } + + [Fact] + public void Approve_NFTokenOwnerSameAsMessageSender_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(ownerAddress, 1)); + } + + [Fact] + public void Approve_ValidApproval_ByApprovedOperator_SwitchesOwnerToApprovedForNFToken() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.Approve(someAddress, 1); + + Assert.Equal(state.GetAddress("IdToApproval:1"), someAddress); + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalLog { Owner = ownerAddress, Approved = someAddress, TokenId = 1 })); + } + + [Fact] + public void Approve_InvalidNFToken_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", Address.Zero); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(someAddress, 1)); + } + + [Fact] + public void TransferFrom_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void TransferFrom_NFTokenOwnerZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", Address.Zero); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(Address.Zero, targetAddress, 1)); + } + + + [Fact] + public void TransferFrom_ValidTokenTransfer_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetAddress("IdToApproval:1", approvalAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void TransferFrom_ValidTokenTransfer_MessageSenderApprovedOwnerToOperator_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void TransferFrom_MessageSenderNotAllowedToCall_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var invalidSenderAddress = "0x0000000000000000000000000000000000000015".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void TransferFrom_TokenDoesNotBelongToFrom_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var notOwningAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(notOwningAddress, targetAddress, 1)); + } + + [Fact] + public void TransferFrom_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(ownerAddress, Address.Zero, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractFalse_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractFalse_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetAddress("IdToApproval:1", approvalAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractFalse_ValidTokenTransfer_MessageSenderApprovedOwnerToOperator_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractTrue_ContractCallReturnsTrue_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + SetupForOnNonFungibleTokenReceived(targetAddress, ownerAddress, ownerAddress, 1).Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance::{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractTrue_ContractCallReturnsTrue_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetAddress("IdToApproval:1", approvalAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + SetupForOnNonFungibleTokenReceived(targetAddress, approvalAddress, ownerAddress, 1).Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractTrue_ContractCallReturnsTrue_ValidTokenTransfer_MessageSenderApprovedOwnerToOperator_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + SetupForOnNonFungibleTokenReceived(targetAddress, operatorAddress, ownerAddress, 1).Returns(TransferResult.Transferred(true)); + + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_MessageSenderNotAllowedToCall_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var invalidSenderAddress = "0x0000000000000000000000000000000000000015".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_NFTokenOwnerZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", Address.Zero); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(Address.Zero, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_TokenDoesNotBelongToFrom_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var notOwningAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(notOwningAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ValidTokenTransfer_ToContractReturnsFalse_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + SetupForOnNonFungibleTokenReceived(targetAddress, ownerAddress, ownerAddress, 1).Returns(TransferResult.Transferred(false)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + private IReturnsThrows SetupForOnNonFungibleTokenReceived(Address targetAddress, Address @operator, Address from, UInt256 tokenId) + { + return transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", It.IsAny(), 0ul)) + .Callback((a, b, c, d, callParams, f) => + { + Assert.True(@operator.Equals(callParams[0])); + Assert.True(from.Equals(callParams[1])); + Assert.True(tokenId.Equals(callParams[2])); + Assert.True(callParams[3] is byte[] bytes && bytes.Length == 0); + }); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractTrue_ContractCallReturnsTruthyObject_CannotCastToBool_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + SetupForOnNonFungibleTokenReceived(targetAddress, ownerAddress, ownerAddress, 1).Returns(TransferResult.Transferred(1)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, Address.Zero, 1)); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractFalse_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractFalse_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetAddress("IdToApproval:1", approvalAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractFalse_ValidTokenTransfer_MessageSenderApprovedOwnerToOperator_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + + transactionExecutorMock.Verify(t => t.Call(It.IsAny(), It.IsAny
(), It.IsAny(), "OnNonFungibleTokenReceived", It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractTrue_ContractCallReturnsTrue_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + var data = new byte[] { 12 }; + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (UInt256)1, data }; + + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", callParamsExpected, 0)) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, data); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractTrue_ContractCallReturnsTrue_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetAddress("IdToApproval:1", approvalAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + var data = new byte[] { 12 }; + var callParamsExpected = new object[] { approvalAddress, ownerAddress, (UInt256)1, data }; + + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", callParamsExpected, 0)) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, data); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractTrue_ContractCallReturnsTrue_ValidTokenTransfer_MessageSenderApprovedOwnerToOperator_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + var data = new byte[] { 12 }; + var callParamsExpected = new object[] { operatorAddress, ownerAddress, (UInt256)1, data }; + + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", callParamsExpected, 0)) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, data); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.True(state.GetBool($"OwnerToOperator:{ownerAddress}:{operatorAddress}")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_MessageSenderNotAllowedToCall_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var invalidSenderAddress = "0x0000000000000000000000000000000000000015".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_NFTokenOwnerZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", Address.Zero); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(Address.Zero, targetAddress, 1, new byte[1] { 0xff })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_TokenDoesNotBelongToFrom_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var notOwningAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(notOwningAddress, targetAddress, 1, new byte[1] { 0xff })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ValidTokenTransfer_ToContractReturnsFalse_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + var data = new byte[] { 12 }; + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (UInt256)1, data }; + + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", callParamsExpected, 0)) + .Returns(TransferResult.Transferred(false)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, data)); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractTrue_ContractCallReturnsTruthyObject_CannotCastToBool_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.IsContractResult = true; + var nonFungibleToken = CreateNonFungibleToken(); + + + var data = new byte[] { 12 }; + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (UInt256)1, data }; + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", callParamsExpected, 0)) + .Returns(TransferResult.Transferred(1)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, data)); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, Address.Zero, 1, new byte[1] { 0xff })); + } + + [Fact] + public void Mint_CalledByNonOwner_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var userAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(userAddress); + + Assert.Throws(() => nonFungibleToken.Mint(userAddress, GetTokenURI(1))); + } + + [Fact] + public void Mint_ToAdressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Mint(Address.Zero, GetTokenURI(1))); + } + + [Fact] + public void Mint_MintingNewToken_Called_By_None_Owner_When_OwnerOnlyMintingFalse_Success() + { + ownerOnlyMinting = false; + + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(targetAddress); + nonFungibleToken.Mint(targetAddress, GetTokenURI(1)); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + Assert.Equal(GetTokenURI(1), nonFungibleToken.TokenURI(1)); + Assert.Equal(1, state.GetUInt256("TokenIdCounter")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = Address.Zero, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void Mint_MintingNewToken_Success() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.Mint(targetAddress, GetTokenURI(1)); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + Assert.Equal(GetTokenURI(1), nonFungibleToken.TokenURI(1)); + Assert.Null(nonFungibleToken.TokenURI(2)); + Assert.Equal(1, state.GetUInt256("TokenIdCounter")); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = Address.Zero, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void SafeMint_MintingNewToken_When_Destination_Is_Contract_Success() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + state.IsContractResult = true; + + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = CreateNonFungibleToken(); + var data = new byte[] { 12 }; + var parameter = new object[] { ownerAddress, Address.Zero, (UInt256)1, data }; + transactionExecutorMock.Setup(t => t.Call(It.IsAny(), targetAddress, 0, "OnNonFungibleTokenReceived", parameter, 0)) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeMint(targetAddress, GetTokenURI(1), data); + + Assert.Equal(targetAddress, state.GetAddress("IdToOwner:1")); + Assert.Equal(1, state.GetUInt256($"Balance:{targetAddress}")); + Assert.Equal(1, state.GetUInt256("TokenIdCounter")); + Assert.Equal(GetTokenURI(1), nonFungibleToken.TokenURI(1)); + Assert.Null(nonFungibleToken.TokenURI(2)); + + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = Address.Zero, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void Burn_NoneExistingToken_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Burn(0)); + } + + [Fact] + public void Burn_BurningNotOwnedToken_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var anotherTokenOwner = "0x0000000000000000000000000000000000000007".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.SetAddress("IdToOwner:0", anotherTokenOwner); + + var nonFungibleToken = CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Burn(0)); + } + + [Fact] + public void Burn_BurningAToken_Success() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + state.SetAddress("IdToOwner:1", ownerAddress); + state.SetUInt256($"Balance:{ownerAddress}", 1); + + var nonFungibleToken = CreateNonFungibleToken(); + + nonFungibleToken.Burn(1); + + Assert.Equal(Address.Zero, state.GetAddress("IdToOwner:1")); + Assert.Equal(0, state.GetUInt256($"Balance:{ownerAddress}")); + Assert.Null(nonFungibleToken.TokenURI(1)); + contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = Address.Zero, TokenId = 1 })); + } + + private NonFungibleToken CreateNonFungibleToken() + { + return new NonFungibleToken(smartContractStateMock.Object, name, symbol, ownerOnlyMinting); + } + } +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/TransferResult.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/TransferResult.cs new file mode 100644 index 00000000..a8ba5eb6 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket.Tests/TransferResult.cs @@ -0,0 +1,16 @@ +using Stratis.SmartContracts; + +namespace NonFungibleTicketContract.Tests +{ + public class TransferResult : ITransferResult + { + public object ReturnValue { get; private set; } + + public bool Success { get; private set; } + + public static TransferResult Failed() => new TransferResult { Success = false }; + + + public static TransferResult Transferred(object returnValue) => new TransferResult { Success = true, ReturnValue = returnValue }; + } +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IAuthorizableRedemptions.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IAuthorizableRedemptions.cs new file mode 100644 index 00000000..7b04ca68 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IAuthorizableRedemptions.cs @@ -0,0 +1,26 @@ +using Stratis.SmartContracts; + +/// +/// Interface for managing perk redemptions on an NFT +/// +public interface IAuthorizableRedemptions +{ + /// + /// Assigns the redeem role to an address, allowing them to redeem ticket perks + /// + /// Address to assign redeem permissions + void AssignRedeemRole(Address address); + + /// + /// Revokes the redeem role from an address, preventing them from redeeming ticket perks + /// + /// Address to revoke redeem permissions + void RevokeRedeemRole(Address address); + + /// + /// Checks if an address has the redeem role and can redeem ticket perks + /// + /// Address to check + /// True if the address has a redeem role; otherwise false + bool CanRedeemPerks(Address address); +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleToken.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleToken.cs new file mode 100644 index 00000000..76770db6 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleToken.cs @@ -0,0 +1,96 @@ +using Stratis.SmartContracts; + +/// +/// Interface for a non-fungible token. +/// +public interface INonFungibleToken +{ + /// + /// Transfers the ownership of an NFT from one address to another address. This function can + /// be changed to payable. + /// + /// Throws unless is the current owner, an authorized operator, or the + /// approved address for this NFT.Throws if 'from' is not the current owner.Throws if 'to' is + /// the zero address.Throws if 'tokenId' is not a valid NFT. When transfer is complete, this + /// function checks if 'to' is a smart contract. If so, it calls + /// 'OnNonFungibleTokenReceived' on 'to' and throws if the return value true. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + /// Additional data with no specified format, sent in call to 'to'. + void SafeTransferFrom(Address from, Address to, UInt256 tokenId, byte[] data); + + /// + /// Transfers the ownership of an NFT from one address to another address. This function can + /// be changed to payable. + /// + /// This works identically to the other function with an extra data parameter, except this + /// function just sets data to an empty byte array. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + void SafeTransferFrom(Address from, Address to, UInt256 tokenId); + + /// + /// Throws unless is the current owner, an authorized operator, or the approved + /// address for this NFT.Throws if is not the current owner.Throws if is the zero + /// address.Throws if is not a valid NFT. This function can be changed to payable. + /// + /// The caller is responsible to confirm that is capable of receiving NFTs or else + /// they maybe be permanently lost. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + void TransferFrom(Address from, Address to, UInt256 tokenId); + + /// + /// Set or reaffirm the approved address for an NFT. This function can be changed to payable. + /// + /// + /// The zero address indicates there is no approved address. Throws unless is + /// the current NFT owner, or an authorized operator of the current owner. + /// + /// Address to be approved for the given NFT ID. + /// ID of the token to be approved. + void Approve(Address approved, UInt256 tokenId); + + /// + /// Enables or disables approval for a third party ("operator") to manage all of + /// 's assets. It also Logs the ApprovalForAll event. + /// + /// This works even if sender doesn't own any tokens at the time. + /// Address to add to the set of authorized operators. + /// True if the operators is approved, false to revoke approval. + void SetApprovalForAll(Address operatorAddress, bool approved); + + /// + /// Returns the number of NFTs owned by 'owner'. NFTs assigned to the zero address are + /// considered invalid, and this function throws for queries about the zero address. + /// + /// Address for whom to query the balance. + /// Balance of owner. + UInt256 BalanceOf(Address owner); + + /// + /// Returns the address of the owner of the NFT. NFTs assigned to zero address are considered invalid, and queries about them do throw. + /// + /// The identifier for an NFT. + /// Address of tokenId owner. + Address OwnerOf(UInt256 tokenId); + + /// + /// Get the approved address for a single NFT. + /// + /// Throws if 'tokenId' is not a valid NFT. + /// ID of the NFT to query the approval of. + /// Address that tokenId is approved for. + Address GetApproved(UInt256 tokenId); + + /// + /// Checks if 'operator' is an approved operator for 'owner'. + /// + /// The address that owns the NFTs. + /// The address that acts on behalf of the owner. + /// True if approved for all, false otherwise. + bool IsApprovedForAll(Address owner, Address operatorAddress); +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenMetadata.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenMetadata.cs new file mode 100644 index 00000000..900b27b0 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenMetadata.cs @@ -0,0 +1,9 @@ +using Stratis.SmartContracts; + +public interface INonFungibleTokenMetadata +{ + string Name { get; } + string Symbol { get; } + + string TokenURI(UInt256 tokenId); +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenReceiver.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenReceiver.cs new file mode 100644 index 00000000..d6090f2f --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/INonFungibleTokenReceiver.cs @@ -0,0 +1,19 @@ +using Stratis.SmartContracts; + +/// +/// Interface for a non-fungible token receiver. +/// +public interface INonFungibleTokenReceiver +{ + /// + /// Handle the receipt of a NFT. The smart contract calls this function on the + /// recipient after a transfer. This function MAY throw or return false to revert and reject the transfer. + /// Return true if the transfer is ok. + /// + /// The address which called safeTransferFrom function. + /// The address which previously owned the token. + /// The NFT identifier which is being transferred. + /// Additional data with no specified format. + /// A bool indicating the resulting operation. + bool OnNonFungibleTokenReceived(Address operatorAddress, Address fromAddress, UInt256 tokenId, byte[] data); +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IRedeemableTicketPerks.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IRedeemableTicketPerks.cs new file mode 100644 index 00000000..eb107209 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/IRedeemableTicketPerks.cs @@ -0,0 +1,30 @@ +using Stratis.SmartContracts; + +/// +/// Interface for redeemable ticket perks on an NFT +/// +/// Supports the redemption of up to 255 perks per token +public interface IRedeemableTicketPerks +{ + /// + /// Retrieves a list of perk redemptions based on a perk + /// + /// Id of the NFT + /// A list of values, either true or false, which represent whether the matching perk (attribute) index is redeemed + bool[] GetRedemptions(UInt256 tokenId); + + /// + /// Retrieves the redemption state of a specific perk + /// + /// Id of the NFT + /// Index of the perk in the NFT attributes + /// True if the perk has been redeemed; otherwise false + bool IsRedeemed(UInt256 tokenId, byte perkIndex); + + /// + /// Performs the redemption of one or more perks + /// + /// Id of the NFT + /// Indexes of the perks in the NFT attributes + void RedeemPerks(UInt256 tokenId, byte[] perksIndexes); +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/ISupportsInterface.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/ISupportsInterface.cs new file mode 100644 index 00000000..19b3ca8f --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/ISupportsInterface.cs @@ -0,0 +1,12 @@ +/// +/// Interface for a class with interface indication support. +/// +public interface ISupportsInterface +{ + /// + /// Function to check which interfaces are supported by this contract. + /// + /// Id of the interface. + /// True if is supported, false otherwise. + bool SupportsInterface(uint interfaceID); +} diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.cs new file mode 100644 index 00000000..914697ea --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.cs @@ -0,0 +1,121 @@ +using Stratis.SmartContracts; + +[Deploy] +public class NonFungibleTicket : NonFungibleToken, IRedeemableTicketPerks, IAuthorizableRedemptions +{ + private const int ByteSize = 256; + + public NonFungibleTicket(ISmartContractState state, string name, string symbol) + : base(state, name, symbol, true) + { + SetSupportedInterfaces(TokenInterface.IRedeemableTicketPerks, true); + SetSupportedInterfaces(TokenInterface.IAuthorizableRedemptions, true); + } + + /// + public bool[] GetRedemptions(UInt256 tokenId) + { + EnsureTokenHasBeenMinted(tokenId); + return GetRedemptionsExecute(tokenId); + } + + /// + public bool IsRedeemed(UInt256 tokenId, byte perkIndex) + { + EnsureTokenHasBeenMinted(tokenId); + return GetRedemptionsExecute(tokenId)?[perkIndex] ?? false; + } + + /// + public void RedeemPerks(UInt256 tokenId, byte[] perkIndexes) + { + Assert(CanRedeemPerks(Message.Sender), "Only assigned addresses can redeem perks."); + EnsureTokenHasBeenMinted(tokenId); + Assert(perkIndexes.Length > 0, "Must provide at least one perk to redeem."); + var redemptions = GetRedemptionsExecute(tokenId); + foreach (var perkIndex in perkIndexes) + { + Assert(!redemptions[perkIndex], $"Perk at index {perkIndex} already redeemed."); + redemptions[perkIndex] = true; + Log(new PerkRedeemedLog { NftId = tokenId, PerkIndex = perkIndex, Redeemer = Message.Sender }); + } + State.SetArray($"Redemptions:{tokenId}", redemptions); + } + + /// + public void AssignRedeemRole(Address address) + { + EnsureOwnerOnly(); + Assert(!CanRedeemPerks(address), "Redeem role is already assigned to this address."); + Log(new RoleAssignedLog { Address = address }); + State.SetBool($"Redeemer:{address}", true); + } + + /// + public void RevokeRedeemRole(Address address) + { + EnsureOwnerOnly(); + Assert(CanRedeemPerks(address), "Redeem role is not assigned to this address."); + Log(new RoleRevokedLog { Address = address }); + State.SetBool($"Redeemer:{address}", false); + } + + /// + public bool CanRedeemPerks(Address address) => State.GetBool($"Redeemer:{address}"); + + /// + /// Retrieves redemptions from state if set; otherwise falls back to the zero-redemptions value + /// + /// Id of the NFT + /// An array of redemption values corresponding to metadata attributes + private bool[] GetRedemptionsExecute(UInt256 tokenId) + { + var redemptions = State.GetArray($"Redemptions:{tokenId}"); + return redemptions.Length != 0 ? redemptions : new bool[ByteSize]; + } + + private void EnsureTokenHasBeenMinted(UInt256 tokenId) => Assert(TokenIdCounter >= tokenId, "Token id does not exist."); + + /// + /// A log that is omitted when a perk is redeemed + /// + public struct PerkRedeemedLog + { + /// + /// Id of the NFT + /// + [Index] public UInt256 NftId; + + /// + /// Index of the perk in the NFT attributes + /// + [Index] public byte PerkIndex; + + /// + /// Address of the staff member that redeemed the perk + /// + public Address Redeemer; + } + + /// + /// A log that is omitted when a redeem role is assigned + /// + public struct RoleAssignedLog + { + /// + /// Address that the role is assigned to + /// + public Address Address; + } + + /// + /// A log that is omitted when a redeem role is revoked + /// + public struct RoleRevokedLog + { + /// + /// Address that the role is revoked from + /// + public Address Address; + } +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.csproj b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.csproj new file mode 100644 index 00000000..08bf6a32 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleTicket.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleToken.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleToken.cs new file mode 100644 index 00000000..9b014116 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/NonFungibleToken.cs @@ -0,0 +1,612 @@ +using Stratis.SmartContracts; + +/// +/// A non fungible token contract. +/// +public class NonFungibleToken : SmartContract, ISupportsInterface, INonFungibleToken, INonFungibleTokenMetadata +{ + /// + /// Function to check which interfaces are supported by this contract. + /// + /// Id of the interface. + /// True if is supported, false otherwise. + public bool SupportsInterface(uint interfaceID) + { + return State.GetBool($"SupportedInterface:{interfaceID}"); + } + + /// + /// Sets the supported interface value. + /// + /// The interface id. + /// A value indicating if the interface id is supported. + protected void SetSupportedInterfaces(TokenInterface interfaceId, bool value) => State.SetBool($"SupportedInterface:{(uint)interfaceId}", value); + + /// + /// Gets the key to the persistent state for the owner by NFT ID. + /// + /// The NFT ID. + /// The persistent storage key to get or set the NFT owner. + private string GetIdToOwnerKey(UInt256 id) => $"IdToOwner:{id}"; + + /// + /// Gets the address of the owner of the NFT ID. + /// + /// The ID of the NFT + ///The owner address. + private Address GetIdToOwner(UInt256 id) => State.GetAddress(GetIdToOwnerKey(id)); + + /// + /// Sets the owner to the NFT ID. + /// + /// The ID of the NFT + /// The address of the owner. + private void SetIdToOwner(UInt256 id, Address value) => State.SetAddress(GetIdToOwnerKey(id), value); + + /// + /// Gets the key to the persistent state for the approval address by NFT ID. + /// + /// The NFT ID. + /// The persistent storage key to get or set the NFT approval. + private string GetIdToApprovalKey(UInt256 id) => $"IdToApproval:{id}"; + + /// + /// Getting from NFT ID the approval address. + /// + /// The ID of the NFT + /// Address of the approval. + private Address GetIdToApproval(UInt256 id) => State.GetAddress(GetIdToApprovalKey(id)); + + /// + /// Setting to NFT ID to approval address. + /// + /// The ID of the NFT + /// The address of the approval. + private void SetIdToApproval(UInt256 id, Address value) => State.SetAddress(GetIdToApprovalKey(id), value); + + /// + /// Gets the amount of non fungible tokens the owner has. + /// + /// The address of the owner. + /// The amount of non fungible tokens. + private UInt256 GetBalance(Address address) => State.GetUInt256($"Balance:{address}"); + + /// + /// Sets the owner count of this non fungible tokens. + /// + /// The address of the owner. + /// The amount of tokens. + private void SetBalance(Address address, UInt256 value) => State.SetUInt256($"Balance:{address}", value); + + /// + /// Gets the permission value of the operator authorization to perform actions on behalf of the owner. + /// + /// The owner address of the NFT. + /// >Address of the authorized operators + /// A value indicating if the operator has permissions to act on behalf of the owner. + private bool GetOwnerToOperator(Address owner, Address operatorAddress) => State.GetBool($"OwnerToOperator:{owner}:{operatorAddress}"); + + /// + /// Sets the owner to operator permission. + /// + /// The owner address of the NFT. + /// >Address to add to the set of authorized operators. + /// The permission value. + private void SetOwnerToOperator(Address owner, Address operatorAddress, bool value) => State.SetBool($"OwnerToOperator:{owner}:{operatorAddress}", value); + + /// + /// Owner of the contract is responsible to for minting/burning + /// + public Address Owner + { + get => State.GetAddress(nameof(Owner)); + private set => State.SetAddress(nameof(Owner), value); + } + + /// + /// Claimed new owner + /// + public Address PendingOwner + { + get => State.GetAddress(nameof(PendingOwner)); + private set => State.SetAddress(nameof(PendingOwner), value); + } + + /// + /// Name for non-fungible token contract + /// + public string Name + { + get => State.GetString(nameof(Name)); + private set => State.SetString(nameof(Name), value); + } + + /// + /// Symbol for non-fungible token contract + /// + public string Symbol + { + get => State.GetString(nameof(Symbol)); + private set => State.SetString(nameof(Symbol), value); + } + + private string GetIdToTokenURI(UInt256 tokenId) => State.GetString($"URI:{tokenId}"); + private void SetIdToTokenURI(UInt256 tokenId, string uri) => State.SetString($"URI:{tokenId}", uri); + + private bool OwnerOnlyMinting + { + get => State.GetBool(nameof(OwnerOnlyMinting)); + set => State.SetBool(nameof(OwnerOnlyMinting), value); + } + + /// + /// The next token id which is going to be minted + /// + protected UInt256 TokenIdCounter + { + get => State.GetUInt256(nameof(TokenIdCounter)); + set => State.SetUInt256(nameof(TokenIdCounter), value); + } + + /// + /// Constructor used to create a new instance of the non-fungible token. + /// + /// The execution state for the contract. + /// Name of the NFT Contract + /// Symbol of the NFT Contract + /// True, if only owner allowed to mint tokens + public NonFungibleToken(ISmartContractState state, string name, string symbol, bool ownerOnlyMinting) : base(state) + { + this.SetSupportedInterfaces(TokenInterface.ISupportsInterface, true); // (ERC165) - ISupportsInterface + this.SetSupportedInterfaces(TokenInterface.INonFungibleToken, true); // (ERC721) - INonFungibleToken, + this.SetSupportedInterfaces(TokenInterface.INonFungibleTokenReceiver, false); // (ERC721) - INonFungibleTokenReceiver + this.SetSupportedInterfaces(TokenInterface.INonFungibleTokenMetadata, true); // (ERC721) - INonFungibleTokenMetadata + this.SetSupportedInterfaces(TokenInterface.INonFungibleTokenEnumerable, false); // (ERC721) - INonFungibleTokenEnumerable + + this.Name = name; + this.Symbol = symbol; + this.Owner = Message.Sender; + this.OwnerOnlyMinting = ownerOnlyMinting; + } + + /// + /// Transfers the ownership of an NFT from one address to another address. This function can + /// be changed to payable. + /// + /// Throws unless is the current owner, an authorized operator, or the + /// approved address for this NFT.Throws if 'from' is not the current owner.Throws if 'to' is + /// the zero address.Throws if 'tokenId' is not a valid NFT. When transfer is complete, this + /// function checks if 'to' is a smart contract. If so, it calls + /// 'OnNonFungibleTokenReceived' on 'to' and throws if the return value true. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + /// Additional data with no specified format, sent in call to 'to'. + public void SafeTransferFrom(Address from, Address to, UInt256 tokenId, byte[] data) + { + SafeTransferFromInternal(from, to, tokenId, data); + } + + /// + /// Transfers the ownership of an NFT from one address to another address. This function can + /// be changed to payable. + /// + /// This works identically to the other function with an extra data parameter, except this + /// function just sets data to an empty byte array. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + public void SafeTransferFrom(Address from, Address to, UInt256 tokenId) + { + SafeTransferFromInternal(from, to, tokenId, new byte[0]); + } + + /// + /// Throws unless is the current owner, an authorized operator, or the approved + /// address for this NFT. + /// Throws if is not the current owner. + /// Throws if is the zero address. + /// Throws if is not a valid NFT. + /// + /// The caller is responsible to confirm that is capable of receiving NFTs or else + /// they maybe be permanently lost. + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + public void TransferFrom(Address from, Address to, UInt256 tokenId) + { + EnsureAddressIsNotEmpty(to); + Address tokenOwner = GetIdToOwner(tokenId); + + EnsureAddressIsNotEmpty(tokenOwner); + CanTransfer(tokenOwner, tokenId); + + Assert(tokenOwner == from, "The from parameter is not token owner."); + + TransferInternal(tokenOwner, to, tokenId); + } + + /// + /// Set or reaffirm the approved address for an NFT. This function can be changed to payable. + /// + /// + /// The zero address indicates there is no approved address. Throws unless is + /// the current NFT owner, or an authorized operator of the current owner. + /// + /// Address to be approved for the given NFT ID. + /// ID of the token to be approved. + public void Approve(Address approved, UInt256 tokenId) + { + Address tokenOwner = GetIdToOwner(tokenId); + + EnsureAddressIsNotEmpty(tokenOwner); + + CanOperate(tokenOwner); + + Assert(approved != tokenOwner, $"The {nameof(approved)} address is already token owner."); + + SetIdToApproval(tokenId, approved); + LogApproval(tokenOwner, approved, tokenId); + } + + /// + /// Enables or disables approval for a third party ("operator") to manage all of + /// 's assets. It also Logs the ApprovalForAll event. + /// + /// This works even if sender doesn't own any tokens at the time. + /// Address to add to the set of authorized operators. + /// True if the operators is approved, false to revoke approval. + public void SetApprovalForAll(Address operatorAddress, bool approved) + { + SetOwnerToOperator(Message.Sender, operatorAddress, approved); + LogApprovalForAll(Message.Sender, operatorAddress, approved); + } + + /// + /// Returns the number of NFTs owned by 'owner'. NFTs assigned to the zero address are + /// considered invalid, and this function throws for queries about the zero address. + /// + /// Address for whom to query the balance. + /// Balance of owner. + public UInt256 BalanceOf(Address owner) + { + EnsureAddressIsNotEmpty(owner); + return GetBalance(owner); + } + + /// + /// Returns the address of the owner of the NFT. NFTs assigned to zero address are considered invalid, and queries about them do throw. + /// + /// The identifier for an NFT. + /// Address of tokenId owner. + public Address OwnerOf(UInt256 tokenId) + { + Address owner = GetIdToOwner(tokenId); + EnsureAddressIsNotEmpty(owner); + return owner; + } + + /// + /// Get the approved address for a single NFT. + /// + /// Throws if 'tokenId' is not a valid NFT. + /// ID of the NFT to query the approval of. + /// Address that tokenId is approved for. + public Address GetApproved(UInt256 tokenId) + { + Address tokenOwner = GetIdToOwner(tokenId); + + EnsureAddressIsNotEmpty(tokenOwner); + + return GetIdToApproval(tokenId); + } + + /// + /// Checks if 'operator' is an approved operator for 'owner'. + /// + /// The address that owns the NFTs. + /// The address that acts on behalf of the owner. + /// True if approved for all, false otherwise. + public bool IsApprovedForAll(Address owner, Address operatorAddress) + { + return GetOwnerToOperator(owner, operatorAddress); + } + + /// + /// Actually performs the transfer. + /// + /// Does NO checks. + /// The current owner of the NFT. + /// Address of a new owner. + /// The NFT that is being transferred. + private void TransferInternal(Address from, Address to, UInt256 tokenId) + { + ClearApproval(tokenId); + + RemoveToken(from, tokenId); + AddToken(to, tokenId); + + LogTransfer(from, to, tokenId); + } + + /// + /// Removes a NFT from owner. + /// + /// Use and override this function with caution. Wrong usage can have serious consequences. + /// Address from wich we want to remove the NFT. + /// Which NFT we want to remove. + private void RemoveToken(Address from, UInt256 tokenId) + { + var tokenCount = GetBalance(from); + SetBalance(from, tokenCount - 1); + SetIdToOwner(tokenId, Address.Zero); + } + + /// + /// Assignes a new NFT to owner. + /// + /// Use and override this function with caution. Wrong usage can have serious consequences. + /// Address to which we want to add the NFT. + /// Which NFT we want to add. + private void AddToken(Address to, UInt256 tokenId) + { + SetIdToOwner(tokenId, to); + var currentTokenAmount = GetBalance(to); + SetBalance(to, currentTokenAmount + 1); + } + + /// + /// Actually perform the safeTransferFrom. + /// + /// The current owner of the NFT. + /// The new owner. + /// The NFT to transfer. + /// Additional data with no specified format, sent in call to 'to' if it is a contract. + private void SafeTransferFromInternal(Address from, Address to, UInt256 tokenId, byte[] data) + { + TransferFrom(from, to, tokenId); + + EnsureContractReceivedToken(from, to, tokenId, data); + } + + /// + /// Clears the current approval of a given NFT ID. + /// + /// ID of the NFT to be transferred + private void ClearApproval(UInt256 tokenId) + { + if (GetIdToApproval(tokenId) != Address.Zero) + { + SetIdToApproval(tokenId, Address.Zero); + } + } + + /// + /// This logs when ownership of any NFT changes by any mechanism. This event logs when NFTs are + /// created('from' == 0) and destroyed('to' == 0). Exception: during contract creation, any + /// number of NFTs may be created and assigned without logging Transfer.At the time of any + /// transfer, the approved Address for that NFT (if any) is reset to none. + /// + /// The from address. + /// The to address. + /// The NFT ID. + private void LogTransfer(Address from, Address to, UInt256 tokenId) + { + Log(new TransferLog() { From = from, To = to, TokenId = tokenId }); + } + + /// + /// This logs when the approved Address for an NFT is changed or reaffirmed. The zero + /// Address indicates there is no approved Address. When a Transfer logs, this also + /// indicates that the approved Address for that NFT (if any) is reset to none. + /// + /// The owner address. + /// The approved address. + /// The NFT ID. + private void LogApproval(Address owner, Address approved, UInt256 tokenId) + { + Log(new ApprovalLog() { Owner = owner, Approved = approved, TokenId = tokenId }); + } + + /// + /// This logs when an operator is enabled or disabled for an owner. The operator can manage all NFTs of the owner. + /// + /// The owner address + /// The operator address. + /// A boolean indicating if it has been approved. + private void LogApprovalForAll(Address owner, Address operatorAddress, bool approved) + { + Log(new ApprovalForAllLog() { Owner = owner, Operator = operatorAddress, Approved = approved }); + } + + + /// + /// Guarantees that the is an owner or operator of the given NFT. + /// + /// ID of the NFT to validate. + private void CanOperate(Address tokenOwner) + { + Assert(IsOwnerOrOperator(tokenOwner), "Caller is not owner nor approved."); + } + + private bool IsOwnerOrOperator(Address tokenOwner) + { + return tokenOwner == Message.Sender || GetOwnerToOperator(tokenOwner, Message.Sender); + } + + /// + /// Guarantees that the msg.sender is allowed to transfer NFT. + /// + /// ID of the NFT to transfer. + private void CanTransfer(Address tokenOwner, UInt256 tokenId) + { + Assert(IsOwnerOrOperator(tokenOwner) || GetIdToApproval(tokenId) == Message.Sender, "Caller is not owner nor approved for token."); + } + + /// + /// Only owner can set new owner and new owner will be in pending state + /// till new owner will call method. + /// + /// The new owner which is going to be in pending state + public void SetPendingOwner(Address newOwner) + { + EnsureOwnerOnly(); + PendingOwner = newOwner; + + Log(new OwnershipTransferRequestedLog { CurrentOwner = Owner, PendingOwner = newOwner }); + } + + /// + /// Waiting be called after new owner is requested by call. + /// Pending owner will be new owner after successfull call. + /// + public void ClaimOwnership() + { + var newOwner = PendingOwner; + + Assert(newOwner == Message.Sender, "ClaimOwnership must be called by the new(pending) owner."); + + var oldOwner = Owner; + Owner = newOwner; + PendingOwner = Address.Zero; + + Log(new OwnershipTransferredLog { PreviousOwner = oldOwner, NewOwner = newOwner }); + } + + protected void EnsureOwnerOnly() + { + Assert(Message.Sender == Owner, "The method is owner only."); + } + + /// + /// Mints new tokens + /// + /// The address that will own the minted NFT + /// Metadata URI of the token + public UInt256 Mint(Address to, string uri) + { + var tokenId = TokenIdCounter += 1; + + Mint(to, tokenId, uri); + + return tokenId; + } + + /// + /// Mints new tokens + /// + /// The address that will own the minted NFT + /// Metadata URI of the token + /// The data param will passed destination contract + public UInt256 SafeMint(Address to, string uri, byte[] data) + { + var tokenId = TokenIdCounter += 1; + + Mint(to, tokenId, uri); + + EnsureContractReceivedToken(Address.Zero, to, tokenId, data); + + return tokenId; + } + + private void Mint(Address to, UInt256 tokenId, string uri) + { + if (OwnerOnlyMinting) + { + EnsureOwnerOnly(); + } + + EnsureAddressIsNotEmpty(to); + + AddToken(to, tokenId); + + SetIdToTokenURI(tokenId, uri); + + LogTransfer(Address.Zero, to, tokenId); + } + + /// + /// Notify contract for received token if destination(to) address is a contract. + /// Raise exception if notification fails. + /// + /// The address which previously owned the token. + /// Destination address that will receive the token + /// The token identifier which is being transferred. + /// Additional data with no specified format. + private void EnsureContractReceivedToken(Address from, Address to, UInt256 tokenId, byte[] data) + { + if (State.IsContract(to)) + { + var result = Call(to, 0, "OnNonFungibleTokenReceived", new object[] { Message.Sender, from, tokenId, data }, 0); + Assert((bool)result.ReturnValue, "OnNonFungibleTokenReceived call failed."); + } + } + + public void Burn(UInt256 tokenId) + { + Address tokenOwner = GetIdToOwner(tokenId); + + Assert(tokenOwner == Message.Sender, "Only token owner can burn the token."); + + ClearApproval(tokenId); + RemoveToken(tokenOwner, tokenId); + + SetIdToTokenURI(tokenId, null); + + LogTransfer(tokenOwner, Address.Zero, tokenId); + } + + public string TokenURI(UInt256 tokenId) => GetIdToTokenURI(tokenId); + + private void EnsureAddressIsNotEmpty(Address address) + { + Assert(address != Address.Zero, "The address can not be zero."); + } + + public struct TransferLog + { + [Index] + public Address From; + [Index] + public Address To; + [Index] + public UInt256 TokenId; + } + + public struct ApprovalLog + { + [Index] + public Address Owner; + [Index] + public Address Approved; + [Index] + public UInt256 TokenId; + } + + public struct ApprovalForAllLog + { + [Index] + public Address Owner; + [Index] + public Address Operator; + + public bool Approved; + } + + public struct OwnershipTransferredLog + { + [Index] + public Address PreviousOwner; + + [Index] + public Address NewOwner; + } + + public struct OwnershipTransferRequestedLog + { + [Index] + public Address CurrentOwner; + [Index] + public Address PendingOwner; + } +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/TokenInterface.cs b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/TokenInterface.cs new file mode 100644 index 00000000..f7b0df23 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTicket/TokenInterface.cs @@ -0,0 +1,10 @@ +public enum TokenInterface +{ + ISupportsInterface = 1, + INonFungibleToken = 2, + INonFungibleTokenReceiver = 3, + INonFungibleTokenMetadata = 4, + INonFungibleTokenEnumerable = 5, + IRedeemableTicketPerks = 6, + IAuthorizableRedemptions = 7 +} \ No newline at end of file diff --git a/Testnet/NonFungibleToken-Ticket/NonFungibleTokenTicketContract.sln b/Testnet/NonFungibleToken-Ticket/NonFungibleTokenTicketContract.sln new file mode 100644 index 00000000..334a52f2 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/NonFungibleTokenTicketContract.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NonFungibleTicket", "NonFungibleTicket\NonFungibleTicket.csproj", "{71600BA5-D603-4A7B-B40E-5D22B8419257}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NonFungibleTicket.Tests", "NonFungibleTicket.Tests\NonFungibleTicket.Tests.csproj", "{6229C49C-4E86-4AD1-9AD1-11ED75065077}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71600BA5-D603-4A7B-B40E-5D22B8419257}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71600BA5-D603-4A7B-B40E-5D22B8419257}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71600BA5-D603-4A7B-B40E-5D22B8419257}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71600BA5-D603-4A7B-B40E-5D22B8419257}.Release|Any CPU.Build.0 = Release|Any CPU + {6229C49C-4E86-4AD1-9AD1-11ED75065077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6229C49C-4E86-4AD1-9AD1-11ED75065077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6229C49C-4E86-4AD1-9AD1-11ED75065077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6229C49C-4E86-4AD1-9AD1-11ED75065077}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Testnet/NonFungibleToken-Ticket/README.md b/Testnet/NonFungibleToken-Ticket/README.md new file mode 100644 index 00000000..0037a851 --- /dev/null +++ b/Testnet/NonFungibleToken-Ticket/README.md @@ -0,0 +1,19 @@ +# Non-Fungible Token Ticket Contract + +**Compiler Version** + +``` +v2.0.0 +``` + +**Contract Hash** + +``` +48f7b38c4f8b6d97507c3ef50e2a9522183d3365ba9ed4bcf555723a6e2779c4 +``` + +**Contract Byte Code** + +``` +4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C010200CC9F8E8E0000000000000000E00022200B013000002C00000002000000000000DE4B00000020000000600000000000100020000000020000040000000000000004000000000000000080000000020000000000000300408500001000001000000000100000100000000000001000000000000000000000008C4B00004F000000000000000000000000000000000000000000000000000000006000000C000000704B00001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000E42B000000200000002C000000020000000000000000000000000000200000602E72656C6F6300000C0000000060000000020000002E0000000000000000000000000000400000420000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C04B0000000000004800000002000500A02A0000D020000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006E02030405172838000006021C17281F000006021D17281F0000062A3E0203280A000006020328090000062A5E0203280A00000602032809000006252D0326162A04912A13300400BE00000001000011020202280D00000A6F0E00000A28080000067201000070280F00000A0203280A00000602048E16FE037255000070280F00000A020328090000060A040B160C2B5A0708910D0206099116FE0172A9000070098C16000001281000000A280F00000A0609179C021204FE150B0000021204037D0A0000041204097D0B000004120402280D00000A6F0E00000A7D0C0000041104280100002B0817580C08078E6932A002281200000A72F1000070038C0F000001281000000A066F1300000A2A0000133003004F0000000200001102284F000006020203280800000616FE017211010070280F00000A021200FE150C0000021200037D0D00000406280200002B02281200000A7273010070038C10000001281000000A176F1400000A2A00133003004C0000000300001102284F0000060202032808000006728D010070280F00000A021200FE150D0000021200037D0E00000406280300002B02281200000A7273010070038C10000001281000000A166F1400000A2A7202281200000A7273010070038C10000001281000000A6F1500000A2A000000133003002D0000000400001102281200000A72F1000070038C0F000001281000000A6F0400002B0A068E2D0B20000100008D1B0000012A062A620202283600000603281700000A72E7010070280F00000A2A7202281200000A7219020070038C1C000001281000000A6F1500000A2A7A02281200000A721902007003B88C1C000001281000000A046F1400000A2A467247020070038C0F000001281000000A2A4E02281200000A020328200000066F1800000A2A5202281200000A02032820000006046F1900000A2A467263020070038C0F000001281000000A2A4E02281200000A020328230000066F1800000A2A5202281200000A02032823000006046F1900000A2A7202281200000A7285020070038C10000001281000000A6F1A00000A2A7602281200000A7285020070038C10000001281000000A046F1B00000A2A8A02281200000A729D020070038C10000001048C10000001281C00000A6F1500000A2A8E02281200000A729D020070038C10000001048C10000001281C00000A056F1400000A2A4602281200000A72CD0200706F1800000A2A4A02281200000A72CD020070036F1900000A2A4602281200000A72D90200706F1800000A2A4A02281200000A72D9020070036F1900000A2A4602281200000A72F30200706F1D00000A2A4A02281200000A72F3020070036F1E00000A2A4602281200000A72FD0200706F1D00000A2A4A02281200000A72FD020070036F1E00000A2A7202281200000A720B030070038C0F000001281000000A6F1D00000A2A7602281200000A720B030070038C0F000001281000000A046F1E00000A2A4602281200000A721B0300706F1500000A2A4A02281200000A721B030070036F1400000A2A4602281200000A723D0300706F1A00000A2A4A02281200000A723D030070036F1B00000A2A001330030057000000000000000203281F00000A021717281F000006021817281F000006021916281F000006021A17281F000006021B16281F0000060204282F000006020528310000060202280D00000A6F0E00000A282B000006020E0428350000062A32020304050E0428450000062A4202030405168D1600000128450000062A000000133004003A0000000500001102042856000006020528210000060A02062856000006020605284C000006020603282000000A725B030070280F00000A0206040528420000062A0000133004004400000005000011020428210000060A020628560000060206284A000006020306282100000A72A903007072F9030070281000000A280F00000A02040328250000060206030428480000062A9E0202280D00000A6F0E00000A030428290000060202280D00000A6F0E00000A030428490000062A3E02032856000006020328260000062A133002001100000005000011020328210000060A02062856000006062A000000133002001700000005000011020328210000060A02062856000006020328240000062A2602030428280000062A8602052846000006020305284300000602040528440000060203040528470000062A00133004002800000006000011020328260000060A02030617282200000A282300000A282700000602047E2400000A28220000062A1330040024000000060000110204032822000006020328260000060A02030617282200000A282500000A28270000062A5602030405283B000006020304050E0428530000062A82020328240000067E2400000A282100000A2C0C02037E2400000A28250000062A00133003002800000007000011021200FE150E0000021200037D0F0000041200047D100000041200057D1100000406280500002B2A133003002800000008000011021200FE150F0000021200037D120000041200047D130000041200057D1400000406280600002B2A133003002800000009000011021200FE15100000021200037D150000041200047D160000041200057D1700000406280700002B2A4E020203284B000006720B040070280F00000A2AA20302280D00000A6F0E00000A282000000A2D13020302280D00000A6F0E00000A28280000062A172ABE020203284B0000062D190204282400000602280D00000A6F0E00000A282000000A2B0117724F040070280F00000A2A00000013300300320000000A00001102284F0000060203282D000006021200FE1512000002120002282A0000067D1A0000041200037D1B00000406280800002B2A0000133003005C0000000B00001102282C0000060A020602280D00000A6F0E00000A282000000A72A7040070280F00000A02282A0000060B0206282B000006027E2400000A282D000006021202FE15110000021202077D180000041202067D1900000408280900002B2A8A0202280D00000A6F0E00000A02282A000006282000000A7219050070280F00000A2A0013300400260000000C0000110202283600000617282200000A282500000A250B2837000006070A020306042852000006062A000013300500340000000C0000110202283600000617282200000A282500000A250B2837000006070A020306042852000006027E2400000A0306052853000006062ACE0228340000062C0602284F0000060203285600000602030428440000060204052833000006027E2400000A030428470000062A13300800660000000D00001102281200000A046F2600000A2C570204166A724D0500701A8D18000001251602280D00000A6F0E00000A8C10000001A22517038C10000001A22518058C0F000001A225190E04A2166A282700000A0A02066F2800000AA51B0000017283050070280F00000A2A0000133004004900000005000011020328210000060A020602280D00000A6F0E00000A282000000A72D3050070280F00000A020328460000060206032843000006020314283300000602067E2400000A0328470000062A22020328320000062A5E02037E2400000A282100000A721D060070280F00000A2A000042534A4201000100000000000C00000076342E302E33303331390000000005006C000000940D0000237E0000000E00006C09000023537472696E6773000000006C1700005806000023555300C41D0000100000002347554944000000D41D0000FC02000023426C6F620000000000000002000001571FA201090A000000FA013300160000010000001C000000120000001B000000560000008A000000050000002800000008000000190000000D00000002000000080000000E0000000100000002000000080000000900000000002A050100000000000600970294070600080394070600F1016A070F00B40700000600DF02E50506007802E50506003502E50506005202E5050600B702E50506000502E50506001C027A030600860569050A00F80264080A00C60164080A00160064080A00540864080600A70169050A00D00264080A008D0864080A00C00864080A003D01640806003C0369050600A103690506009B0869050A00DA01640806000009690506008B056905060001006905000000001E000000000001000100A100000014010000000001000100010100000501000031000100020001001000A2080000280009000200A10000000208000000000A000B00A10000005B00000000000A000E00A1000000EB07000000000A001100A1000000B805000000000A001400A10000001207000000000A001D0001001000B90500004D000A001E000A011000F203000045000A0057000A0110000204000045000D0057000A011000E303000045000E0057000A0110006604000045000F0057000A011000480400004500120057000A011000540400004500150057000A011000120400004500180057000A0110002A04000045001A0057000606530066015680140169015680B805690156801207690156805B0069015680520169015680EB07690156800208690151805F0366010600A8006D010600E008710106004506D90006005408D90006005408D90006007C05D90006000806D900060098006D010600E006D9000600F000D900060098006D010600E006D90006005B07D9000600F00074010600B906D9000600D406D9000600C706D90006008706D900000000000000C605150177010100502000000000861864077C0102006C2000000000E6011B08840105007C2000000000E601AE008B010600942000000000E601DF0792010800602100000000E6017F019A010A00BC2100000000E6016E019A010B00142200000000E601DC071F010C003422000000008100260384010D006D22000000008100B900A0010E00000000000000C6057F019A010F00000000000000C6056E019A011000000000000000C605DC071F011100000000000000C60D9001A6011200000000000000C60D3C05A6011200000000000000C6054A00AA011200000000000000C6051B0884011300000000000000C605AE008B011400000000000000C605DF0792011600000000000000C6057005B0011800000000000000C6057005BC011C00000000000000C6057405BC011F00000000000000C6055703C6012200000000000000C6051305CE012400000000000000C6056803D5012600000000000000C6057203DC012700000000000000C605ED00DC012800000000000000C605F004E3012900000000000000C605D200EB012B00862200000000E601150177012F00A3220000000084007D07F7013000C2220000000081001909AA013200D4220000000081009F06DC013300E822000000008100AC06FE013400FD220000000081000609AA0136000F23000000008100C204DC0137002323000000008100D204FE01380038230000000081002701D5013A0055230000000081003201C6013B0073230000000081002C07E3013D0096230000000081003F0706023F00BA230000000086084E0625004200CC2300000000810858069A014200DF23000000008608620625004300F12300000000810873069A014300042400000000E6099001A60144001624000000008108990110004400292400000000E6093C05A60145003B240000000081084705100045004E240000000081003300AA0146006B2400000000810043000F0247008924000000008108A803160249009B24000000008108BD031A024900AE24000000008408EC061F024A00C024000000008408FF06A0014A00D424000000008618640724024B00372500000000E6017005B0014F00442500000000E6017005BC015300582500000000E6017405BC015600A02500000000E6015703C6015900F02500000000E6011305CE015B00182600000000E6016803D5015D00282600000000E6017203DC015E00482600000000E601ED00DC015F006B2600000000E601F004E301600075260000000081008F04BC0162009826000000008100CA05C6016500CC260000000081009305C6016700FC260000000081007604B00169001227000000008100E204A0016D0034270000000081002D06BC016E006827000000008100B604BC0171009C27000000008100010506027400D027000000008100B1019A017700E42700000000810052071F0178000D280000000081003906C6017900402800000000860084069A017B008028000000008600130606007C00E828000000008400290906007C000C29000000008600D4082D027C004029000000008600D00835027E008029000000008100D4083F028100B4290000000081009C05B0018400282A0000000086000306A00188007D2A00000000E6014A00AA018900862A00000000810053099A018A0000000100270000000100EB0100000200A20100000300520500000100A00000000100A00000000200EA0800000100A00000000200C307000001005C08000001005C08000001005C0800000100A00000000100A000000001005C08000001005C08000001005C0800000100A00000000100A00000000100A00000000200EA0800000100A00000000200CF0700000100810500000200100600000300A00000000400700000000100810500000200100600000300A00000000100810500000200100600000300A00000000100F90000000200A00000000100360800000200F90000000100E60600000100A00000000100A00000000100E606000002003608000001003608000002002A0800000300A000000004007000000001002700000001008C00000002005103000001000201000001000201000001000201000002005103000001000201000001000201000001000201000002005103000001005C08000001005C0800000200510300000100E60600000200360800000100E60600000200360800000300510300000100510300000100510300000100510300000100510300000100A00000000100A00000000200720400000100510300000100510300000100EB0100000200A20100000300520500000400D20300000100810500000200100600000300A00000000400700000000100810500000200100600000300A00000000100810500000200100600000300A00000000100F90000000200A00000000100360800000200F90000000100E60600000100A00000000100A00000000100E60600000200360800000100810500000200100600000300A00000000100810500000200A00000000100100600000200A00000000100810500000200100600000300A00000000400700000000100A00000000100810500000200100600000300A00000000100E60600000200F90000000300A00000000100E60600000200360800000300F90000000100940600000100940600000100940600000200A00000000100DD0600000100100600000200720400000100100600000200720400000300700000000100100600000200A00000000300720400000100810500000200100600000300A00000000400700000000100A00000000100A000000001005C0804001C00040014000A0008000A0020000A001800090064070100110064070600190064070A00290064071000310064071000390064071000410064071000490064071000510064071000590064071000690064070600910064070600990046012000A900220625009900D9082A00B9007B08300099006E0436009900BC014200C900FD084700C90061055800C90059056800C900F40872007900A0047E00C90046088600C90051088C00C90008009300C90013009900B9007B08A000C9009403A700C9009E03AC0099006407B20081003909BD0081004509BD007900B408CA007900D605D00081000B06D9007900F705D000C90082081F01990025052501A10041033101080008003E0108000C004301080010004801080014004D0108001800520108001C005701080020005C010800240061012E000B005A022E00130063022E001B0082022E0023008B022E002B00A2022E003300AD022E003B00BA022E0043008B022E004B008B022E005300C50283005B003E01410163003E01610163003E01E10163003E01010263003E01210263003E01410263003E01610263003E01810263003E01A10263003E01C10263003E01010363003E01210363003E01410363003E01610363003E0115004E005E006D00B800C500DD00E700F100FB00050113011A01060001000A00030000009D01480200004B0548020000E0064C02000087064C0200009D01480200004B0548020000C103510200000307550202000E00030002000F00050002002A00070001002B00070002002C00090001002D00090002002E000B0001002F000B00020030000D00010031000D00020034000F00010035000F000200360011000100370011000480000001000000000000000000000000008D080000040000000000000000000000350175000000000002000000000000000000000000006408000000000B0004000C0004000D0004000E000A000F000A0010000A0011000A0012000A0023003D0023005300230063002D007A002300E2002300EC002300F6002300000123000E0100000055496E7433320047657455496E743235360053657455496E74323536003C4D6F64756C653E00696E746572666163654944004765744964546F546F6B656E555249005365744964546F546F6B656E5552490076616C75655F5F00494E6F6E46756E6769626C65546F6B656E4D657461646174610053797374656D2E507269766174652E436F72654C696200696E74657266616365496400546F6B656E496400746F6B656E4964004E6674496400497352656465656D656400456E73757265546F6B656E4861734265656E4D696E746564004F6E4E6F6E46756E6769626C65546F6B656E526563656976656400476574417070726F76656400617070726F76656400696400546F6B656E496E746572666163650049537570706F727473496E746572666163650047657442616C616E63650053657442616C616E636500494D657373616765006765745F4D65737361676500494E6F6E46756E6769626C65546F6B656E456E756D657261626C65005265766F6B6552656465656D526F6C650041737369676E52656465656D526F6C65006765745F4E616D65007365745F4E616D65006E616D650056616C7565547970650043616E4F706572617465006765745F53746174650049536D617274436F6E74726163745374617465004950657273697374656E7453746174650073746174650044656275676761626C6541747472696275746500417373656D626C795469746C65417474726962757465005461726765744672616D65776F726B41747472696275746500417373656D626C7946696C6556657273696F6E41747472696275746500417373656D626C79496E666F726D6174696F6E616C56657273696F6E41747472696275746500417373656D626C79436F6E66696775726174696F6E41747472696275746500436F6D70696C6174696F6E52656C61786174696F6E7341747472696275746500417373656D626C7950726F6475637441747472696275746500496E64657841747472696275746500417373656D626C79436F6D70616E79417474726962757465004465706C6F794174747269627574650052756E74696D65436F6D7061746962696C69747941747472696275746500476574526564656D7074696F6E73457865637574650042797465006765745F52657475726E56616C75650076616C756500417070726F7665004279746553697A650042616C616E63654F66004F776E65724F660053797374656D2E52756E74696D652E56657273696F6E696E6700476574537472696E6700536574537472696E67006765745F4F776E65724F6E6C794D696E74696E67007365745F4F776E65724F6E6C794D696E74696E67006F776E65724F6E6C794D696E74696E6700526F6C655265766F6B65644C6F67005065726B52656465656D65644C6F6700526F6C6541737369676E65644C6F67004F776E6572736869705472616E736665727265644C6F67004F776E6572736869705472616E736665725265717565737465644C6F6700417070726F76616C4C6F6700417070726F76616C466F72416C6C4C6F67005472616E736665724C6F670075726900536166655472616E7366657246726F6D496E7465726E616C005472616E73666572496E7465726E616C006F705F477265617465725468616E4F72457175616C004C6F67417070726F76616C004765744964546F417070726F76616C005365744964546F417070726F76616C00436C656172417070726F76616C004973417070726F766564466F72416C6C004C6F67417070726F76616C466F72416C6C00536574417070726F76616C466F72416C6C0043616C6C00536D617274436F6E74726163742E646C6C006765745F53796D626F6C007365745F53796D626F6C0073796D626F6C00476574426F6F6C00536574426F6F6C0053797374656D00536166655472616E7366657246726F6D0066726F6D00456E756D00426F6F6C65616E00416464546F6B656E00456E73757265436F6E74726163745265636569766564546F6B656E00494E6F6E46756E6769626C65546F6B656E0052656D6F7665546F6B656E006F705F5375627472616374696F6E0053797374656D2E5265666C656374696F6E006F705F4164646974696F6E004275726E00546F005A65726F00746F00436C61696D4F776E657273686970006765745F53656E646572004C6F675472616E736665720043616E5472616E736665720052656465656D6572006765745F4F776E6572007365745F4F776E6572006765745F50656E64696E674F776E6572007365745F50656E64696E674F776E65720053657450656E64696E674F776E657200746F6B656E4F776E6572004765744964546F4F776E6572005365744964546F4F776E65720050726576696F75734F776E65720043757272656E744F776E6572004E65774F776E6572006E65774F776E6572006F776E6572006765745F546F6B656E4964436F756E746572007365745F546F6B656E4964436F756E74657200494E6F6E46756E6769626C65546F6B656E5265636569766572004765744F776E6572546F4F70657261746F72005365744F776E6572546F4F70657261746F720049734F776E65724F724F70657261746F72002E63746F720053797374656D2E446961676E6F737469637300536574537570706F72746564496E74657266616365730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300446562756767696E674D6F646573007065726B496E6465786573007065726B73496E64657865730043616E52656465656D5065726B73004952656465656D61626C655469636B65745065726B730049417574686F72697A61626C65526564656D7074696F6E7300476574526564656D7074696F6E730066726F6D41646472657373006F70657261746F724164647265737300476574416464726573730053657441646472657373006164647265737300537472617469732E536D617274436F6E74726163747300466F726D6174004973436F6E747261637400536D617274436F6E7472616374004F626A656374004E6F6E46756E6769626C655469636B6574006F705F496D706C6963697400495472616E73666572526573756C7400536166654D696E7400417373657274005065726B496E646578007065726B496E646578004765744172726179005365744172726179004765744964546F417070726F76616C4B6579004765744964546F4F776E65724B657900456E737572654F776E65724F6E6C79006F705F457175616C697479006F705F496E657175616C69747900456E737572654164647265737349734E6F74456D707479000000534F006E006C0079002000610073007300690067006E006500640020006100640064007200650073007300650073002000630061006E002000720065006400650065006D0020007000650072006B0073002E0000534D007500730074002000700072006F00760069006400650020006100740020006C00650061007300740020006F006E00650020007000650072006B00200074006F002000720065006400650065006D002E0000475000650072006B00200061007400200069006E0064006500780020007B0030007D00200061006C00720065006100640079002000720065006400650065006D00650064002E00001F52006500640065006D007000740069006F006E0073003A007B0030007D000061520065006400650065006D00200072006F006C006500200069007300200061006C00720065006100640079002000610073007300690067006E0065006400200074006F0020007400680069007300200061006400640072006500730073002E000019520065006400650065006D00650072003A007B0030007D000059520065006400650065006D00200072006F006C00650020006900730020006E006F0074002000610073007300690067006E0065006400200074006F0020007400680069007300200061006400640072006500730073002E00003154006F006B0065006E00200069006400200064006F006500730020006E006F0074002000650078006900730074002E00002D53007500700070006F00720074006500640049006E0074006500720066006100630065003A007B0030007D00001B4900640054006F004F0077006E00650072003A007B0030007D0000214900640054006F0041007000700072006F00760061006C003A007B0030007D000017420061006C0061006E00630065003A007B0030007D00002F4F0077006E006500720054006F004F00700065007200610074006F0072003A007B0030007D003A007B0031007D00000B4F0077006E00650072000019500065006E00640069006E0067004F0077006E006500720000094E0061006D006500000D530079006D0062006F006C00000F5500520049003A007B0030007D0000214F0077006E00650072004F006E006C0079004D0069006E00740069006E006700001D54006F006B0065006E004900640043006F0075006E00740065007200004D5400680065002000660072006F006D00200070006100720061006D00650074006500720020006900730020006E006F007400200074006F006B0065006E0020006F0077006E00650072002E00004F54006800650020007B0030007D0020006100640064007200650073007300200069007300200061006C0072006500610064007900200074006F006B0065006E0020006F0077006E00650072002E00001161007000700072006F007600650064000043430061006C006C006500720020006900730020006E006F00740020006F0077006E006500720020006E006F007200200061007000700072006F007600650064002E000057430061006C006C006500720020006900730020006E006F00740020006F0077006E006500720020006E006F007200200061007000700072006F00760065006400200066006F007200200074006F006B0065006E002E00007143006C00610069006D004F0077006E0065007200730068006900700020006D007500730074002000620065002000630061006C006C0065006400200062007900200074006800650020006E00650077002800700065006E00640069006E006700290020006F0077006E00650072002E00003354006800650020006D006500740068006F00640020006900730020006F0077006E006500720020006F006E006C0079002E0000354F006E004E006F006E00460075006E006700690062006C00650054006F006B0065006E0052006500630065006900760065006400004F4F006E004E006F006E00460075006E006700690062006C00650054006F006B0065006E00520065006300650069007600650064002000630061006C006C0020006600610069006C00650064002E0000494F006E006C007900200074006F006B0065006E0020006F0077006E00650072002000630061006E0020006200750072006E002000740068006500200074006F006B0065006E002E000039540068006500200061006400640072006500730073002000630061006E0020006E006F00740020006200650020007A00650072006F002E000000630D3E04678F654BB5787D98657CE40500042001010803200001052001011111042001010E0A07051D021D050805112C0420001255042000114105200201020E0500020E0E1C06300101011E00040A01112C0420001265062002010E12690407011130040A011130052002010E020407011134040A011134042001020E0407011D02073001011D1E000E030A010207000202113D113D05200111410E062002010E1141052001113D0E062002010E113D0600030E0E1C1C0420010E0E052002010E0E05200101123904070111410700020211411141040701113D050001113D08080002113D113D113D030611410407011138040A011138040701113C040A01113C0407011140040A0111400407011148040A011148080703114111411144040A011144060702113D113D04070112510520010211410B2005125111410B0E1D1C0B0320001C087CEC85D7BEA7798E040100000004020000000403000000040400000004050000000406000000040700000004000100000206080306110C0306113D02060502060204200102090720030112390E0E0620011D02113D06200202113D0507200201113D1D0505200101114105200101113D0320000E0520010E113D0B20040111411141113D1D050920030111411141113D072002011141113D06200201114102062001113D11410620011141113D07200202114111410B20040211411141113D1D0506200201110C0207200201113D114108200301114111410206200201113D0E032000020420010102042000113D0820040112390E0E02072002113D11410E092003113D11410E1D05082003011141113D0E0328000E042800114103280002042800113D0801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F777301080100020000000000160100114E6F6E46756E6769626C655469636B657400000A010005446562756700000C010007312E302E302E3000000A010005312E302E300000350100182E4E4554436F72654170702C56657273696F6E3D76332E310100540E144672616D65776F726B446973706C61794E616D65000000000000000000000000000010000000000000000000000000000000B44B00000000000000000000CE4B0000002000000000000000000000000000000000000000000000C04B0000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF250020001000000000000000000000000000000000000000000000000000000000004000000C000000E03B00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +```