diff --git a/src/NonFungibleToken/.gitignore b/src/NonFungibleToken/.gitignore new file mode 100644 index 0000000..959502c --- /dev/null +++ b/src/NonFungibleToken/.gitignore @@ -0,0 +1,333 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +.vs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +*.sol \ No newline at end of file diff --git a/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenContract.Tests.csproj b/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenContract.Tests.csproj new file mode 100644 index 0000000..411bf1f --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenContract.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + + + + diff --git a/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenTests.cs b/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenTests.cs new file mode 100644 index 0000000..234aece --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken.Tests/NonFungibleTokenTests.cs @@ -0,0 +1,1242 @@ +using System; +using System.Collections.Generic; +using Moq; +using Stratis.SmartContracts; +using Stratis.SmartContracts.CLR; +using Xunit; + +public class NonFungibleTokenTests +{ + private Mock smartContractStateMock; + private Mock contractLoggerMock; + private Mock persistentStateMock; + private Dictionary supportedInterfaces; + private Dictionary idToOwner; + private Dictionary idToApproval; + private Dictionary ownerToOperator; + private Dictionary ownerToNFTokenCount; + private Mock internalTransactionExecutorMock; + + public NonFungibleTokenTests() + { + this.contractLoggerMock = new Mock(); + this.persistentStateMock = new Mock(); + this.smartContractStateMock = new Mock(); + this.internalTransactionExecutorMock = new Mock(); + this.smartContractStateMock.Setup(s => s.PersistentState).Returns(this.persistentStateMock.Object); + this.smartContractStateMock.Setup(s => s.ContractLogger).Returns(this.contractLoggerMock.Object); + this.smartContractStateMock.Setup(x => x.InternalTransactionExecutor).Returns(this.internalTransactionExecutorMock.Object); + + this.supportedInterfaces = new Dictionary(); + this.idToOwner = new Dictionary(); + this.idToApproval = new Dictionary(); + this.ownerToOperator = new Dictionary(); + this.ownerToNFTokenCount = new Dictionary(); + + this.SetupPersistentState(); + } + + [Fact] + public void Constructor_Sets_SupportedInterfaces() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Equal(3, this.supportedInterfaces.Count); + Assert.True(this.supportedInterfaces["SupportedInterface:1"]); + Assert.True(this.supportedInterfaces["SupportedInterface:2"]); + Assert.False(this.supportedInterfaces["SupportedInterface:3"]); + } + + [Fact] + public void SupportsInterface_InterfaceSupported_ReturnsTrue() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.SupportsInterface(2); + + Assert.True(result); + } + + [Fact] + public void SupportsInterface_InterfaceSetToFalseSupported_ReturnsFalse() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + this.supportedInterfaces["SupportedInterface:2"] = false; + + var result = nonFungibleToken.SupportsInterface(3); + + Assert.False(result); + } + + [Fact] + public void SupportsInterface_InterfaceNotSupported_ReturnsFalse() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.SupportsInterface(4); + + Assert.False(result); + } + + [Fact] + public void GetApproved_NotValidNFToken_OwnerAddressZero_ThrowsException() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.GetApproved(1)); + } + + [Fact] + public void GetApproved_ApprovalNotInStorage_ReturnsZeroAddress() + { + this.idToOwner.Add("IdToOwner:1", "0x0000000000000000000000000000000000000005".HexToAddress()); + this.idToApproval.Clear(); + + var nonFungibleToken = this.CreateNonFungibleToken(); + var result = nonFungibleToken.GetApproved(1); + + Assert.Equal(Address.Zero, result); + } + + [Fact] + public void GetApproved_ApprovalInStorage_ReturnsAddress() + { + var approvalAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", "0x0000000000000000000000000000000000000005".HexToAddress()); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + var result = nonFungibleToken.GetApproved(1); + + Assert.Equal(approvalAddress, result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorInStateAsTrue_ReturnsTrue() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddresss}", true); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.True(result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorInStateAsFalse_ReturnsFalse() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddresss}", false); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.False(result); + } + + [Fact] + public void IsApprovedForAll_OwnerToOperatorNotInState_ReturnsFalse() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddresss = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.ownerToOperator.Clear(); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.IsApprovedForAll(ownerAddress, operatorAddresss); + + Assert.False(result); + } + + [Fact] + public void OwnerOf_IdToOwnerNotInStorage_ThrowsException() + { + this.idToOwner.Clear(); + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.OwnerOf(1)); + } + + [Fact] + public void OwnerOf_NFTokenMappedToAddressZero_ThrowsException() + { + this.idToOwner.Add("IdToOwner:1", Address.Zero); + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.OwnerOf(1)); + } + + [Fact] + public void OwnerOf_NFTokenExistsWithOwner_ReturnsOwnerAddress() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.OwnerOf(1); + + Assert.Equal(ownerAddress, result); + } + + [Fact] + public void BalanceOf_OwnerZero_ThrowsException() + { + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => { nonFungibleToken.BalanceOf(Address.Zero); }); + } + + [Fact] + public void BalanceOf_NftTokenCountNotInStorage_ReturnsZero() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.ownerToNFTokenCount.Clear(); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.BalanceOf(ownerAddress); + + Assert.Equal((ulong)0, result); + } + + [Fact] + public void BalanceOf_OwnerNftTokenCountInStorage_ReturnsTokenCount() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 15); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var result = nonFungibleToken.BalanceOf(ownerAddress); + + Assert.Equal((ulong)15, result); + } + + [Fact] + public void SetApprovalForAll_SetsMessageSender_ToOperatorApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var nonFungibleToken = this.CreateNonFungibleToken(); + + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + nonFungibleToken.SetApprovalForAll(operatorAddress, true); + + Assert.NotEmpty(this.ownerToOperator); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalForAllLog { Owner = ownerAddress, Operator = operatorAddress, Approved = true })); + } + + [Fact] + public void Approve_TokenOwnerNotMessageSenderOrOperator_ThrowsException() + { + this.idToOwner.Clear(); + this.ownerToOperator.Clear(); + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(someAddress, 1)); + } + + [Fact] + public void Approve_ValidApproval_SwitchesOwnerToApprovedForNFToken() + { + this.idToApproval.Clear(); + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.Approve(someAddress, 1); + + Assert.NotEmpty(this.idToApproval); + Assert.Equal(this.idToApproval["IdToApproval:1"], someAddress); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalLog { Owner = ownerAddress, Approved = someAddress, TokenId = 1 })); + } + + [Fact] + public void Approve_NTFokenOwnerSameAsMessageSender_ThrowsException() + { + this.idToApproval.Clear(); + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(ownerAddress, 1)); + } + + [Fact] + public void Approve_ValidApproval_ByApprovedOperator_SwitchesOwnerToApprovedForNFToken() + { + this.idToApproval.Clear(); + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.Approve(someAddress, 1); + + Assert.NotEmpty(this.idToApproval); + Assert.Equal(this.idToApproval["IdToApproval:1"], someAddress); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.ApprovalLog { Owner = ownerAddress, Approved = someAddress, TokenId = 1 })); + } + + [Fact] + public void Approve_InvalidNFToken_ThrowsException() + { + this.idToApproval.Clear(); + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var operatorAddress = Address.Zero; + var someAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", Address.Zero); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.Approve(someAddress, 1)); + } + + [Fact] + public void TransferFrom_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + } + + [Fact] + public void TransferFrom_ValidTokenTransfer_MessageSenderApprovedForTokenIdByOwner_TransfersTokenFrom_To_ClearsApproval() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var approvalAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Clear(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void TransferFrom_NFTokenOwnerZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", Address.Zero); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(Address.Zero, targetAddress, 1)); + } + + [Fact] + public void TransferFrom_TokenDoesNotBelongToFrom_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + var notOwningAddress = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.TransferFrom(notOwningAddress, targetAddress, 1)); + } + + [Fact] + public void TransferFrom_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Clear(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.Empty((byte[])callParams[3]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { approvalAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.Empty((byte[])callParams[3]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Clear(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { operatorAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.Empty((byte[])callParams[3]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_NFTokenOwnerZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", Address.Zero); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(notOwningAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ValidTokenTransfer_ToContractReturnsFalse_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.Empty((byte[])callParams[3]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(false)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToContractTrue_ContractCallReturnsTruthyObject_CannotCastToBool_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.Empty((byte[])callParams[3]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(1)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1)); + } + + [Fact] + public void SafeTransferFrom_NoDataProvided_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Clear(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(false); + var nonFungibleToken = this.CreateNonFungibleToken(); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.contractLoggerMock.Verify(l => l.Log(It.IsAny(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 })); + + this.internalTransactionExecutorMock.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.NotEmpty((byte[])callParams[3]); + Assert.Equal(0xff, ((byte[])callParams[3])[0]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Add("IdToApproval:1", approvalAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(approvalAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { approvalAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.NotEmpty((byte[])callParams[3]); + Assert.Equal(0xff, ((byte[])callParams[3])[0]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.idToApproval.Clear(); + this.ownerToOperator.Add($"OwnerToOperator:{ownerAddress}:{operatorAddress}", true); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(operatorAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { operatorAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.NotEmpty((byte[])callParams[3]); + Assert.Equal(0xff, ((byte[])callParams[3])[0]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(true)); + + nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff }); + + Assert.Equal(targetAddress, this.idToOwner["IdToOwner:1"]); + Assert.Empty(this.idToApproval); + Assert.True(this.ownerToOperator[$"OwnerToOperator:{ownerAddress}:{operatorAddress}"]); + Assert.Equal((ulong)0, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{ownerAddress}"]); + Assert.Equal((ulong)1, this.ownerToNFTokenCount[$"OwnerToNFTokenCount:{targetAddress}"]); + this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(invalidSenderAddress); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", Address.Zero); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(Address.Zero); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.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(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.NotEmpty((byte[])callParams[3]); + Assert.Equal(0xff, ((byte[])callParams[3])[0]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(false)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToContractTrue_ContractCallReturnsTruthyObject_CannotCastToBool_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + this.persistentStateMock.Setup(p => p.IsContract(targetAddress)) + .Returns(true); + var nonFungibleToken = this.CreateNonFungibleToken(); + + var callParamsExpected = new object[] { ownerAddress, ownerAddress, (ulong)1, new byte[0] }; + this.internalTransactionExecutorMock.Setup( + t => t.Call( + It.IsAny(), + targetAddress, + 0, + "OnNonFungibleTokenReceived", + It.IsAny(), + It.IsAny())) + .Callback((a, b, c, d, callParams, f) => + { + Assert.Equal(callParamsExpected[0], callParams[0]); + Assert.Equal(callParamsExpected[1], callParams[1]); + Assert.Equal(callParamsExpected[2], callParams[2]); + Assert.NotEmpty((byte[])callParams[3]); + Assert.Equal(0xff, ((byte[])callParams[3])[0]); + Assert.Equal(typeof(byte[]), callParams[3].GetType()); + }) + .Returns(TransferResult.Transferred(1)); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, targetAddress, 1, new byte[1] { 0xff })); + } + + [Fact] + public void SafeTransferFrom_DataProvided_ToAddressZero_ThrowsException() + { + var ownerAddress = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.idToOwner.Add("IdToOwner:1", ownerAddress); + this.ownerToNFTokenCount.Add($"OwnerToNFTokenCount:{ownerAddress}", 1); + this.smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress); + + var nonFungibleToken = this.CreateNonFungibleToken(); + + Assert.Throws(() => nonFungibleToken.SafeTransferFrom(ownerAddress, Address.Zero, 1, new byte[1] { 0xff })); + } + + private NonFungibleToken CreateNonFungibleToken() + { + return new NonFungibleToken(this.smartContractStateMock.Object); + } + + private void SetupPersistentState() + { + this.SetupSupportedInterfaces(); + this.SetupIdToOwner(); + this.SetupIdToApproval(); + this.SetupOwnerToOperators(); + this.SetupOwnerToNFTokenCount(); + } + + private void SetupOwnerToNFTokenCount() + { + this.persistentStateMock.Setup(p => p.SetUInt64(It.Is(s => s.StartsWith("OwnerToNFTokenCount:", StringComparison.Ordinal)), It.IsAny())) + .Callback((key, value) => + { + if (this.ownerToNFTokenCount.ContainsKey(key)) + { + this.ownerToNFTokenCount[key] = value; + } + else + { + this.ownerToNFTokenCount.Add(key, value); + } + }); + this.persistentStateMock.Setup(p => p.GetUInt64(It.Is(s => s.StartsWith("OwnerToNFTokenCount:")))) + .Returns((key) => + { + if (this.ownerToNFTokenCount.ContainsKey(key)) + { + return this.ownerToNFTokenCount[key]; + } + + return default(ulong); + }); + } + + private void SetupOwnerToOperators() + { + this.persistentStateMock.Setup(p => p.SetBool(It.Is(s => s.StartsWith("OwnerToOperator:", StringComparison.Ordinal)), It.IsAny())) + .Callback((key, value) => + { + if (this.ownerToOperator.ContainsKey(key)) + { + this.ownerToOperator[key] = value; + } + else + { + this.ownerToOperator.Add(key, value); + } + }); + this.persistentStateMock.Setup(p => p.GetBool(It.Is(s => s.StartsWith("OwnerToOperator:")))) + .Returns((key) => + { + if (this.ownerToOperator.ContainsKey(key)) + { + return this.ownerToOperator[key]; + } + + return default(bool); + }); + } + + private void SetupIdToApproval() + { + this.persistentStateMock.Setup(p => p.SetAddress(It.Is(s => s.StartsWith("IdToApproval:", StringComparison.Ordinal)), It.IsAny
())) + .Callback((key, value) => + { + if (this.idToApproval.ContainsKey(key)) + { + this.idToApproval[key] = value; + } + else + { + this.idToApproval.Add(key, value); + } + }); + this.persistentStateMock.Setup(p => p.GetAddress(It.Is(s => s.StartsWith("IdToApproval:")))) + .Returns((key) => + { + if (this.idToApproval.ContainsKey(key)) + { + return this.idToApproval[key]; + } + + return Address.Zero; + }); + + this.persistentStateMock.Setup(p => p.Clear(It.Is(s => s.StartsWith("IdToApproval:")))) + .Callback((key) => + { + this.idToApproval.Remove(key); + }); + } + + private void SetupIdToOwner() + { + this.persistentStateMock.Setup(p => p.SetAddress(It.Is(s => s.StartsWith("IdToOwner:", StringComparison.Ordinal)), It.IsAny
())) + .Callback((key, value) => + { + if (this.idToOwner.ContainsKey(key)) + { + this.idToOwner[key] = value; + } + else + { + this.idToOwner.Add(key, value); + } + }); + this.persistentStateMock.Setup(p => p.GetAddress(It.Is(s => s.StartsWith("IdToOwner:")))) + .Returns((key) => + { + if (this.idToOwner.ContainsKey(key)) + { + return this.idToOwner[key]; + } + + return Address.Zero; + }); + + this.persistentStateMock.Setup(p => p.Clear(It.Is(s => s.StartsWith("IdToOwner:")))) + .Callback((key) => + { + this.idToOwner.Remove(key); + }); + } + + private void SetupSupportedInterfaces() + { + this.persistentStateMock.Setup(p => p.SetBool(It.Is(s => s.StartsWith("SupportedInterface:", StringComparison.Ordinal)), It.IsAny())) + .Callback((key, value) => + { + if (this.supportedInterfaces.ContainsKey(key)) + { + this.supportedInterfaces[key] = value; + } + else + { + this.supportedInterfaces.Add(key, value); + } + }); + this.persistentStateMock.Setup(p => p.GetBool(It.Is(s => s.StartsWith("SupportedInterface:")))) + .Returns((key) => + { + if (this.supportedInterfaces.ContainsKey(key)) + { + return this.supportedInterfaces[key]; + } + + return default(bool); + }); + } +} \ No newline at end of file diff --git a/src/NonFungibleToken/NonFungibleToken/INonFungibleToken.cs b/src/NonFungibleToken/NonFungibleToken/INonFungibleToken.cs new file mode 100644 index 0000000..dcdb99b --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken/INonFungibleToken.cs @@ -0,0 +1,99 @@ +namespace NonFungibleTokenContract +{ + 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, ulong 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, ulong 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, ulong 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, ulong 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. + ulong 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(ulong 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(ulong 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/src/NonFungibleToken/NonFungibleToken/INonFungibleTokenReceiver.cs b/src/NonFungibleToken/NonFungibleToken/INonFungibleTokenReceiver.cs new file mode 100644 index 0000000..78290e7 --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken/INonFungibleTokenReceiver.cs @@ -0,0 +1,22 @@ +namespace NonFungibleTokenContract +{ + 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, ulong tokenId, byte[] data); + } +} diff --git a/src/NonFungibleToken/NonFungibleToken/ISupportsInterface.cs b/src/NonFungibleToken/NonFungibleToken/ISupportsInterface.cs new file mode 100644 index 0000000..9ea804f --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken/ISupportsInterface.cs @@ -0,0 +1,15 @@ +namespace NonFungibleTokenContract +{ + /// + /// 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/src/NonFungibleToken/NonFungibleToken/NonFungibleToken.cs b/src/NonFungibleToken/NonFungibleToken/NonFungibleToken.cs new file mode 100644 index 0000000..e48ab20 --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken/NonFungibleToken.cs @@ -0,0 +1,472 @@ +using Stratis.SmartContracts; +using System; + +/// +/// A non fungible token contract. +/// +public class NonFungibleToken : SmartContract +{ + public struct TransferLog + { + [Index] + public Address From; + [Index] + public Address To; + [Index] + public ulong TokenId; + } + + public struct ApprovalLog + { + [Index] + public Address Owner; + [Index] + public Address Approved; + [Index] + public ulong TokenId; + } + + public struct ApprovalForAllLog + { + [Index] + public Address Owner; + [Index] + public Address Operator; + + public bool Approved; + } + + /// + /// Get a value indicacting if the interface is supported. + /// + /// The id of the interface to support. + /// A value indicating if the interface is supported. + private bool GetSupportedInterfaces(uint interfaceId) + { + return this.PersistentState.GetBool($"SupportedInterface:{interfaceId}"); + } + + /// + /// Sets the supported interface value. + /// + /// The interface id. + /// A value indicating if the interface id is supported. + private void SetSupportedInterfaces(uint interfaceId, bool value) + { + this.PersistentState.SetBool($"SupportedInterface:{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(ulong id) + { + return $"IdToOwner:{id}"; + } + + /// + /// Gets the address of the owner of the NFT ID. + /// + /// The ID of the NFT + ///The owner address. + private Address GetIdToOwner(ulong id) + { + return this.PersistentState.GetAddress(GetIdToOwnerKey(id)); + } + + /// + /// Sets the owner to the NFT ID. + /// + /// The ID of the NFT + /// The address of the owner. + private void SetIdToOwner(ulong id, Address value) + { + this.PersistentState.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(ulong id) + { + return $"IdToApproval:{id}"; + } + + /// + /// Getting from NFT ID the approval address. + /// + /// The ID of the NFT + /// Address of the approval. + private Address GetIdToApproval(ulong id) + { + return this.PersistentState.GetAddress(GetIdToApprovalKey(id)); + } + + /// + /// Setting to NFT ID to approval address. + /// + /// The ID of the NFT + /// The address of the approval. + private void SetIdToApproval(ulong id, Address value) + { + this.PersistentState.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 ulong GetOwnerToNFTokenCount(Address address) + { + return this.PersistentState.GetUInt64($"OwnerToNFTokenCount:{address}"); + } + + /// + /// Sets the owner count of this non fungible tokens. + /// + /// The address of the owner. + /// The amount of tokens. + private void SetOwnerToNFTokenCount(Address address, ulong value) + { + this.PersistentState.SetUInt64($"OwnerToNFTokenCount:{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) + { + return this.PersistentState.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) + { + this.PersistentState.SetBool($"OwnerToOperator:{owner}:{operatorAddress}", value); + } + + /// + /// Constructor. Initializes the supported interfaces. + /// + /// The smart contract state. + public NonFungibleToken(ISmartContractState state) : base(state) + { + // todo: discuss callback handling and supported interface numbering with community. + this.SetSupportedInterfaces((uint)0x00000001, true); // (ERC165) - ISupportsInterface + this.SetSupportedInterfaces((uint)0x00000002, true); // (ERC721) - INonFungibleToken, + this.SetSupportedInterfaces((uint)0x00000003, false); // (ERC721) - INonFungibleTokenReceiver + } + + /// + /// 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 GetSupportedInterfaces(interfaceID); + } + + /// + /// 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, ulong 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, ulong 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. 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. + public void TransferFrom(Address from, Address to, ulong tokenId) + { + CanTransfer(tokenId); + ValidNFToken(tokenId); + + Address tokenOwner = GetIdToOwner(tokenId); + Assert(tokenOwner == from); + Assert(to != Address.Zero); + + TransferInternal(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, ulong tokenId) + { + CanOperate(tokenId); + ValidNFToken(tokenId); + + Address tokenOwner = GetIdToOwner(tokenId); + Assert(approved != tokenOwner); + + 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(this.Message.Sender, operatorAddress, approved); + LogApprovalForAll(this.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 ulong BalanceOf(Address owner) + { + Assert(owner != Address.Zero); + return GetOwnerToNFTokenCount(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(ulong tokenId) + { + Address owner = GetIdToOwner(tokenId); + Assert(owner != Address.Zero); + 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(ulong tokenId) + { + ValidNFToken(tokenId); + + 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 preforms the transfer. + /// + /// Does NO checks. + /// Address of a new owner. + /// The NFT that is being transferred. + private void TransferInternal(Address to, ulong tokenId) + { + Address from = GetIdToOwner(tokenId); + ClearApproval(tokenId); + + RemoveNFToken(from, tokenId); + AddNFToken(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 RemoveNFToken(Address from, ulong tokenId) + { + Assert(GetIdToOwner(tokenId) == from); + SetOwnerToNFTokenCount(from, checked(GetOwnerToNFTokenCount(from) - 1)); + this.PersistentState.Clear(GetIdToOwnerKey(tokenId)); + } + + /// + /// 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 AddNFToken(Address to, ulong tokenId) + { + Assert(GetIdToOwner(tokenId) == Address.Zero); + + SetIdToOwner(tokenId, to); + ulong currentTokenAmount = GetOwnerToNFTokenCount(to); + SetOwnerToNFTokenCount(to, checked(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, ulong tokenId, byte[] data) + { + CanTransfer(tokenId); + ValidNFToken(tokenId); + + Address tokenOwner = GetIdToOwner(tokenId); + Assert(tokenOwner == from); + Assert(to != Address.Zero); + + TransferInternal(to, tokenId); + + if (this.PersistentState.IsContract(to)) + { + ITransferResult result = this.Call(to, 0, "OnNonFungibleTokenReceived", new object[] { this.Message.Sender, from, tokenId, data }, 0); + Assert((bool)result.ReturnValue); + } + } + + /// + /// Clears the current approval of a given NFT ID. + /// + /// ID of the NFT to be transferred + private void ClearApproval(ulong tokenId) + { + if (GetIdToApproval(tokenId) != Address.Zero) + { + this.PersistentState.Clear(GetIdToApprovalKey(tokenId)); + } + } + + /// + /// 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, ulong 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, ulong 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(ulong tokenId) + { + Address tokenOwner = GetIdToOwner(tokenId); + Assert(tokenOwner == this.Message.Sender || GetOwnerToOperator(tokenOwner, this.Message.Sender)); + } + + /// + /// Guarantees that the msg.sender is allowed to transfer NFT. + /// + /// ID of the NFT to transfer. + private void CanTransfer(ulong tokenId) + { + Address tokenOwner = GetIdToOwner(tokenId); + Assert( + tokenOwner == this.Message.Sender + || GetIdToApproval(tokenId) == Message.Sender + || GetOwnerToOperator(tokenOwner, Message.Sender) + ); + } + + /// + /// Guarantees that tokenId is a valid Token. + /// + /// ID of the NFT to validate. + private void ValidNFToken(ulong tokenId) + { + Assert(GetIdToOwner(tokenId) != Address.Zero); + } +} \ No newline at end of file diff --git a/src/NonFungibleToken/NonFungibleToken/NonFungibleTokenContract.csproj b/src/NonFungibleToken/NonFungibleToken/NonFungibleTokenContract.csproj new file mode 100644 index 0000000..f511226 --- /dev/null +++ b/src/NonFungibleToken/NonFungibleToken/NonFungibleTokenContract.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp2.2 + + + + + + + + diff --git a/src/NonFungibleToken/NonFungibleTokenContract.sln b/src/NonFungibleToken/NonFungibleTokenContract.sln new file mode 100644 index 0000000..e069dd0 --- /dev/null +++ b/src/NonFungibleToken/NonFungibleTokenContract.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29201.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonFungibleTokenContract", "NonFungibleToken\NonFungibleTokenContract.csproj", "{D64B8959-5CC0-43D4-99B7-E07481222B5D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NonFungibleTokenContract.Tests", "NonFungibleToken.Tests\NonFungibleTokenContract.Tests.csproj", "{855863D4-4F60-47D0-AD2A-164749950614}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D64B8959-5CC0-43D4-99B7-E07481222B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D64B8959-5CC0-43D4-99B7-E07481222B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D64B8959-5CC0-43D4-99B7-E07481222B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D64B8959-5CC0-43D4-99B7-E07481222B5D}.Release|Any CPU.Build.0 = Release|Any CPU + {855863D4-4F60-47D0-AD2A-164749950614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {855863D4-4F60-47D0-AD2A-164749950614}.Debug|Any CPU.Build.0 = Debug|Any CPU + {855863D4-4F60-47D0-AD2A-164749950614}.Release|Any CPU.ActiveCfg = Release|Any CPU + {855863D4-4F60-47D0-AD2A-164749950614}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {70BE9CE1-C24C-48EC-861C-D3FDEA2628FD} + EndGlobalSection +EndGlobal