Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TestNet] Add Delegatedtransfer method to NonFungibleToken #95

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.12.0" />
<PackageReference Include="Stratis.SmartContracts.CLR" Version="2.0.2" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using FluentAssertions;
using Moq;
using Moq.Language.Flow;
using NBitcoin;
using NBitcoin.Networks;
using NonFungibleTokenContract.Tests;
using Stratis.SmartContracts;
using System;
Expand All @@ -15,24 +17,37 @@ public class NonFungibleTokenTests
private Mock<IContractLogger> contractLoggerMock;
private InMemoryState state;
private Mock<IInternalTransactionExecutor> transactionExecutorMock;
private ISerializer serializer;
private Address contractAddress;
private Network network;
private string name;
private string symbol;
private bool ownerOnlyMinting;

class ChameleonNetwork : Network
{
public ChameleonNetwork(byte base58Prefix)
{
this.Base58Prefixes = new byte[][] { new byte[] { base58Prefix } };
}
}

public NonFungibleTokenTests()
{
this.contractLoggerMock = new Mock<IContractLogger>();
this.smartContractStateMock = new Mock<ISmartContractState>();
this.transactionExecutorMock = new Mock<IInternalTransactionExecutor>();
this.serializer = new Stratis.SmartContracts.CLR.Serialization.Serializer(new Stratis.SmartContracts.CLR.Serialization.ContractPrimitiveSerializerV2(null));
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.smartContractStateMock.Setup(x => x.Serializer).Returns(this.serializer);
this.contractAddress = "0x0000000000000000000000000000000000000001".HexToAddress();
this.name = "Tickets Token";
this.symbol = "TCKT";
this.ownerOnlyMinting = true;
this.network = new ChameleonNetwork(119);
}

public string GetTokenURI(UInt256 tokenId) => $"https://example.com/api/tokens/{tokenId}";
Expand Down Expand Up @@ -590,6 +605,39 @@ public void TransferFrom_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To(
contractLoggerMock.Verify(l => l.Log(It.IsAny<ISmartContractState>(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 }));
}

private string AddressToString(Address address)
{
var address160 = Stratis.SmartContracts.CLR.AddressExtensions.ToUint160(address);
return Stratis.SmartContracts.CLR.AddressExtensions.ToBase58Address(address160, this.network);
}

[Fact]
public void DelegatedTransfer_ValidTokenTransfer_MessageSender_TransfersTokenFrom_To()
{
var key = new Key();
var ownerAddress = Convert.ToHexString(key.PubKey.Hash.ToBytes()).HexToAddress();
var targetAddress = "0x0000000000000000000000000000000000000007".HexToAddress();
var contractAddress = "0x0000000000000000000000000000000000000000".HexToAddress();
state.SetAddress("IdToOwner:1", ownerAddress);
state.SetUInt256($"Balance:{ownerAddress}", 1);

smartContractStateMock.Setup(m => m.Message.Sender).Returns(ownerAddress);

var nonFungibleToken = CreateNonFungibleToken();
var uid = Guid.NewGuid();

string url = $"?uid={Convert.ToHexString(uid.ToByteArray().Reverse().ToArray())}&contract={this.AddressToString(contractAddress)}&method=DelegatedTransfer&from={this.AddressToString(ownerAddress)}&to={this.AddressToString(targetAddress)}&tokenId=1";
byte[] signature = Convert.FromBase64String(key.SignMessage(url));

nonFungibleToken.DelegatedTransfer(url, signature);

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<ISmartContractState>(), new NonFungibleToken.TransferLog { From = ownerAddress, To = targetAddress, TokenId = 1 }));
}

[Fact]
public void TransferFrom_NFTokenOwnerZero_ThrowsException()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
using Stratis.SmartContracts;
using Stratis.SCL.Crypto;

/// <summary>
/// A non fungible token contract.
/// </summary>
public class NonFungibleToken : SmartContract
{
/// <summary>
/// Function to check for replays of signed transfers.
/// </summary>
/// <param name="transferID">A unique number identifying the transfer.</param>
/// <returns>True if the transfer had already been performed, false otherwise.</returns>
public bool KnownTransfer(UInt128 transferID) => State.GetBool($"Transfer:{transferID}");

/// <summary>
/// Records the <paramref name="transferID"/> of a signed transfer.
/// </summary>
/// <param name="transferID">A unique number identifying the transfer.</param>
private void SetKnownTransfer(UInt128 transferID) => State.SetBool($"Transfer:{transferID}", true);

/// <summary>
/// Function to check which interfaces are supported by this contract.
Expand Down Expand Up @@ -215,6 +229,52 @@ public void SafeTransferFrom(Address from, Address to, UInt256 tokenId, byte[] d
SafeTransferFromInternal(from, to, tokenId, data);
}

/// <summary>
/// <para>Throws if <see cref="signature"/> can't be resolved from <see cref="url"/>.</para>
/// <para>Throws if the following <see cref="url"/> fields are invalid:
/// <list type="bullet">
/// <item>Throws if "method" is not "DelegatedTransfer".</item>
/// <item>Throws if "contract" is not this.Address.</item>
/// <item>Throws if "uid" has successfully been used before.</item>
/// <item>Throws if "from" is not the current owner or has a different address prexix from the "to" address or contract address.</item>
/// <item>Throws if "to" is the zero address or has a different address prexix from the "to" address or contract address.</item>
/// <item>Throws if "tokenId" is not a valid NFT or does not belong to the signee.</item>
/// </list></para>
/// </summary>
/// <remarks>The caller is responsible to confirm that <see cref="to"/> is capable of receiving NFTs or else
/// they maybe be permanently lost.</remarks>
/// <param name="url">The url containing the method arguments.</param>
/// <param name="signature">The signature of the <paramref name="url"/> string signed by the owner.</param>
public void DelegatedTransfer(string url, byte[] signature)
{
string[] args = SSAS.GetURLArguments(url, new string[] { "uid", "contract", "method", "from", "to", "tokenId" });

Assert(args != null && args.Length == 6 && args[2] == nameof(DelegatedTransfer), "Invalid url.");
Assert(Serializer.ToAddress(SSAS.ParseAddress(args[1], out byte prefix0)) == this.Address, "Invalid contract address.");

var uniqueNumber = UInt128.Parse($"0x{args[0]}");
Assert(!KnownTransfer(uniqueNumber), "The transfer has already been performed.");

var tokenId = UInt256.Parse(args[5]);
Assert(SSAS.TryGetSignerSHA256(Serializer.Serialize(url), signature, out Address signer), "Could not resolve signer.");
Assert(signer == GetIdToOwner(tokenId), "Invalid signature.");

// "ParseAddress" should work regardless of whether main or test address strings are passed.
var from = Serializer.ToAddress(SSAS.ParseAddress(args[3], out byte prefix1));
var to = Serializer.ToAddress(SSAS.ParseAddress(args[4], out byte prefix2));
Assert(prefix1 == prefix2, "'From' and 'To' address prefixes are different.");
Assert(prefix1 == prefix0, "Contract address versus 'From' and 'To' address prefixes are different.");

// Allow Message.Sender to perform the transfer.
SetIdToApproval(tokenId, Message.Sender);

SetKnownTransfer(uniqueNumber);

TransferFrom(from, to, tokenId);

LogDelegatedTransfer(from, to, tokenId, uniqueNumber, signature);
}

/// <summary>
/// Transfers the ownership of an NFT from one address to another address. This function can
/// be changed to payable.
Expand Down Expand Up @@ -423,6 +483,11 @@ private void LogTransfer(Address from, Address to, UInt256 tokenId)
Log(new TransferLog() { From = from, To = to, TokenId = tokenId });
}

private void LogDelegatedTransfer(Address from, Address to, UInt256 tokenId, UInt128 uniqueNumber, byte[] signature)
{
Log(new DelegatedTransferLog() { From = from, To = to, TokenId = tokenId, UniqueNumber = uniqueNumber, Signature = signature });
}

/// <summary>
/// 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
Expand Down Expand Up @@ -612,6 +677,19 @@ private enum TokenInterface
ITicketContract = 100,
}

public struct DelegatedTransferLog
{
[Index]
public Address From;
[Index]
public Address To;
[Index]
public UInt256 TokenId;

public UInt128 UniqueNumber;
public byte[] Signature;
}

public struct TransferLog
{
[Index]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Stratis.SmartContracts" Version="2.0.0" />
<PackageReference Include="Stratis.SmartContracts.Standards" Version="1.0.0" />
<PackageReference Include="Stratis.SmartContracts.Standards" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\StratisFullNode-1\src\Stratis.SCL\Stratis.SCL.csproj" />
</ItemGroup>

</Project>
10 changes: 8 additions & 2 deletions Testnet/NonFungibleToken-Ticket/NonFungibleTokenContract.sln
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29201.188
# Visual Studio Version 17
VisualStudioVersion = 17.4.33403.182
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stratis.SCL", "..\..\..\StratisFullNode-1\src\Stratis.SCL\Stratis.SCL.csproj", "{D0399ECC-6A43-46F4-91AF-37B901661426}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +23,10 @@ Global
{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
{D0399ECC-6A43-46F4-91AF-37B901661426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0399ECC-6A43-46F4-91AF-37B901661426}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0399ECC-6A43-46F4-91AF-37B901661426}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0399ECC-6A43-46F4-91AF-37B901661426}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading