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

Add NFT templete and UT #268

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Neo.SmartContract.Framework/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ public static byte ToByte(this int source)
return (byte)(source + 0);
}

public static bool Equals(this byte[] left, byte[] right)
{
if (left.Length != right.Length) return false;
if (left == null || right == null) return false;
shargon marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i < left.Length; i++)
{
if (left[i] != right[i]) return false;
}
return true;
}

[OpCode(OpCode.CAT)]
public extern static byte[] Concat(this byte[] first, byte[] second);

Expand Down
160 changes: 160 additions & 0 deletions templates/Template.NFT.CSharp/NFTContract.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Services.Neo;
using System;
using System.ComponentModel;
using System.Numerics;
using Helper = Neo.SmartContract.Framework.Helper;

namespace NFTContract
{
/// <summary>
/// Non-Fungible Token Smart Contract Template
/// </summary>
public class NFTContract : SmartContract
{
[DisplayName("MintedToken")]
public static event Action<byte[], byte[], byte[]> MintedToken;//(byte[] to, byte[] tokenId, byte[] properties);
ShawnYun marked this conversation as resolved.
Show resolved Hide resolved

[DisplayName("Transferred")]
shargon marked this conversation as resolved.
Show resolved Hide resolved
public static event Action<byte[], byte[], BigInteger, byte[]> Transferred; //(byte[] from , byte[] to, BigInteger amount, byte[] TokenId)

//super admin address
private static readonly byte[] superAdmin = Helper.ToScriptHash("Nj9Epc1x2sDmd6yH5qJPYwXRqSRf5X6KHE");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there possibility to change superAdmin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better not to change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contractOwner?

private static string TotalSupplyKey() => "totalSupply";

private static StorageContext Context() => Storage.CurrentContext;
private static byte[] TokenOwnerKey(byte[] tokenId, byte[] owner) => new byte[] { 0x10 }.Concat(tokenId).Concat(owner);
private static byte[] TotalBalanceKey(byte[] owner) => new byte[] { 0x11 }.Concat(owner);
private static byte[] TokenBalanceKey(byte[] owner, byte[] tokenId) => new byte[] { 0x12 }.Concat(owner).Concat(tokenId);
private static byte[] PropertiesKey(byte[] tokenId) => new byte[] { 0x13 }.Concat(tokenId);

private const int TOKEN_DECIMALS = 8;
private const int MAX_AMOUNT = 100_000_000;

public static string Name()
{
return "MyNFT";
ShawnYun marked this conversation as resolved.
Show resolved Hide resolved
}

public static string Symbol()
{
return "MNFT";
}

public static string SupportedStandards()
{
return "{\"NEP-10\", \"NEP-11\"}";
shargon marked this conversation as resolved.
Show resolved Hide resolved
}

public static BigInteger TotalSupply()
{
return Storage.Get(Context(), TotalSupplyKey()).ToBigInteger();
}

public static int Decimals()
{
return TOKEN_DECIMALS;
}

public static Enumerator<byte[]> OwnerOf(byte[] tokenid)
{
return Storage.Find(Context(), new byte[] { 0x10 }.Concat(tokenid)).Values;
}

public static Enumerator<byte[]> TokensOf(byte[] owner)
{
if (owner.Length != 20) throw new FormatException("The parameter 'owner' should be 20-byte address.");
return Storage.Find(Context(), new byte[] { 0x10 }.Concat(owner)).Values;
}

public static byte[] Properties(byte[] tokenid)
ShawnYun marked this conversation as resolved.
Show resolved Hide resolved
{
return Storage.Get(Context(), PropertiesKey(tokenid));
}

ShawnYun marked this conversation as resolved.
Show resolved Hide resolved
public static bool MintNFT(byte[] tokenId, byte[] owner, byte[] properties)
ShawnYun marked this conversation as resolved.
Show resolved Hide resolved
{
if (!Runtime.CheckWitness(superAdmin)) return false;

if (owner.Length != 20) throw new FormatException("The parameter 'owner' should be 20-byte address."); ;
if (properties.Length > 2048) throw new FormatException("The length of 'properties' should be less than 2048."); ;

if (Storage.Get(Context(), TokenOwnerKey(tokenId, owner)) != null) return false;

Storage.Put(Context(), PropertiesKey(tokenId), properties);
shargon marked this conversation as resolved.
Show resolved Hide resolved
Storage.Put(Context(), TokenOwnerKey(tokenId, owner), owner);

var totalSupplyKey = TotalSupplyKey();
var totalSupply = Storage.Get(Context(), totalSupplyKey);
if (totalSupply is null)
Storage.Put(Context(), totalSupplyKey, 1);
else
Storage.Put(Context(), totalSupplyKey, totalSupply.ToBigInteger() + 1);

var tokenBalanceKey = TokenBalanceKey(owner, tokenId);
Storage.Put(Context(), tokenBalanceKey, MAX_AMOUNT);

var totalBalanceKey = TotalBalanceKey(owner);
var totalBalance = Storage.Get(Context(), totalBalanceKey);
if (totalBalance is null)
Storage.Put(Context(), totalBalanceKey, MAX_AMOUNT);
else
Storage.Put(Context(), totalBalanceKey, totalBalance.ToBigInteger() + MAX_AMOUNT);

//notify
MintedToken(owner, tokenId, properties);
return true;
}

public static BigInteger BalanceOf(byte[] owner, byte[] tokenid)
{
if (owner.Length != 20) throw new FormatException("The parameter 'owner' should be 20-byte address.");
if (tokenid is null)
return Storage.Get(Context(), TotalBalanceKey(owner)).ToBigInteger();
else
return Storage.Get(Context(), TokenBalanceKey(owner, tokenid)).ToBigInteger();
}

public static bool Transfer(byte[] from, byte[] to, BigInteger amount, byte[] tokenId)
{
if (from.Length != 20 || to.Length != 20) throw new FormatException("The parameters 'from' and 'to' should be 20-byte addresses.");
if (amount < 0 || amount > MAX_AMOUNT) throw new FormatException("The parameters 'amount' is out of range.");

if (!Runtime.CheckWitness(from)) return false;

if (from.Equals(to))
{
Transferred(from, to, amount, tokenId);
return true;
}

var fromTokenBalance = Storage.Get(Context(), TokenBalanceKey(from, tokenId));
var fromTotalBalance = Storage.Get(Context(), TotalBalanceKey(from));
if (fromTokenBalance == null || fromTokenBalance.ToBigInteger() < amount) return false;
var fromNewBalance = fromTokenBalance.ToBigInteger() - amount;
if (fromNewBalance == 0)
{
Storage.Delete(Context(), TokenOwnerKey(tokenId, from));
}
Storage.Put(Context(), TokenBalanceKey(from, tokenId), fromNewBalance);
Storage.Put(Context(), TotalBalanceKey(from), fromTotalBalance.ToBigInteger() - amount);

var toTokenBalance = Storage.Get(Context(), TokenBalanceKey(to, tokenId));
var toTotalBalance = Storage.Get(Context(), TotalBalanceKey(to));
if (toTokenBalance is null && amount > 0)
{
Storage.Put(Context(), TokenOwnerKey(tokenId, to), to);
Storage.Put(Context(), TokenBalanceKey(to, tokenId), amount);
}
else
{
Storage.Put(Context(), TokenBalanceKey(to, tokenId), toTokenBalance.ToBigInteger() + amount);
Storage.Put(Context(), TotalBalanceKey(to), toTotalBalance.ToBigInteger() + amount);
}

//notify
Transferred(from, to, amount, tokenId);
return true;
}
}
}
11 changes: 11 additions & 0 deletions templates/Template.NFT.CSharp/Template.NFT.CSharp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Neo.SmartContract.Framework\Neo.SmartContract.Framework.csproj" />
</ItemGroup>

</Project>
159 changes: 159 additions & 0 deletions tests/Template.NEP5.UnitTests/UnitTest_NFT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Neo.Compiler.MSIL.UnitTests.Utils;
using Neo.IO;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.VM.Types;
using Neo.SmartContract.Iterators;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Neo.SmartContract.Framework.UnitTests
{
[TestClass]
public class NFTTest
{
private TestEngine _engine;

[TestInitialize]
public void Init()
{
_engine = new TestEngine(TriggerType.Application, new TestScriptContainer(), null);
_engine.AddEntryScript("../../../../../templates/Template.NFT.CSharp/NFTContract.cs");
}
[TestMethod]
public void Test_GetName()
{
var result = _engine.ExecuteTestCaseStandard("name").Pop();

StackItem wantResult = "MyNFT";
Assert.AreEqual(wantResult.ConvertTo(StackItemType.ByteString), result.ConvertTo(StackItemType.ByteString));
}

[TestMethod]
public void Test_GetDecimals()
{
var result = _engine.ExecuteTestCaseStandard("decimals").Pop();

var wantResult = 8;
Assert.AreEqual(wantResult, result as Integer);
}

[TestMethod]
public void Test_MintNFT()
{
var hash = _engine.CurrentScriptHash;
var snapshot = _engine.Snapshot as TestSnapshot;

snapshot.Contracts.Add(hash, new Ledger.ContractState()
{
Manifest = new Manifest.ContractManifest()
{
Features = Manifest.ContractFeatures.HasStorage
}
});

var tokenid = System.Text.Encoding.Default.GetBytes("abc");
var owner = Wallets.Helper.ToScriptHash("Nj9Epc1x2sDmd6yH5qJPYwXRqSRf5X6KHE").ToArray();
var to = Wallets.Helper.ToScriptHash("NTegNkUTqL5UUqb5MjsHP4cbXftkhuZA1p").ToArray();
var properties = "NFT properties";

// Mint NFT
var result = _engine.ExecuteTestCaseStandard("mintNFT", tokenid, owner, properties).Pop();
var wantResult = true;
Assert.AreEqual(wantResult, result.ConvertTo(StackItemType.Boolean));

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("totalSupply").Pop();
var wantTotalSupply = 1;
Assert.AreEqual(wantTotalSupply, result);

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("properties", tokenid).Pop();
Assert.AreEqual(properties, result);

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", owner, Null.Null).Pop();
var wantBalance = 100_000_000;
Assert.AreEqual(wantBalance, result);

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", owner, tokenid).Pop();
wantBalance = 100_000_000;
Assert.AreEqual(wantBalance, result);

// Mint new NFT
_engine.Reset();
var tokenid2 = System.Text.Encoding.Default.GetBytes("def");
result = _engine.ExecuteTestCaseStandard("mintNFT", tokenid2, owner, properties).Pop();
wantResult = true;
Assert.AreEqual(wantResult, result.ConvertTo(StackItemType.Boolean));

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("totalSupply").Pop();
wantTotalSupply = 2;
Assert.AreEqual(wantTotalSupply, result);

// Balance of all
_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", owner, Null.Null).Pop();
wantBalance = 200_000_000;
Assert.AreEqual(wantBalance, result);

// Balance of token2
_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", owner, tokenid2).Pop();
wantBalance = 100_000_000;
Assert.AreEqual(wantBalance, result);

// Transfer
_engine.Reset();
result = _engine.ExecuteTestCaseStandard("transfer", owner, to, 10_000_000, tokenid2).Pop();
Assert.AreEqual(true, result.ConvertTo(StackItemType.Boolean));

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", owner, tokenid2).Pop();
wantBalance = 90_000_000;
Assert.AreEqual(wantBalance, result);

_engine.Reset();
result = _engine.ExecuteTestCaseStandard("balanceOf", to, tokenid2).Pop();
wantBalance = 10_000_000;
Assert.AreEqual(wantBalance, result);
}
}

internal class TestScriptContainer : IVerifiable
{
public Witness[] Witnesses { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }

public int Size => throw new System.NotImplementedException();

public void Deserialize(BinaryReader reader)
{
throw new System.NotImplementedException();
}

public void DeserializeUnsigned(BinaryReader reader)
{
throw new System.NotImplementedException();
}

public UInt160[] GetScriptHashesForVerifying(StoreView snapshot)
{
var hash = Wallets.Helper.ToScriptHash("Nj9Epc1x2sDmd6yH5qJPYwXRqSRf5X6KHE");
return new UInt160[] { hash };
}

public void Serialize(BinaryWriter writer)
{
throw new System.NotImplementedException();
}

public void SerializeUnsigned(BinaryWriter writer)
{
throw new System.NotImplementedException();
}
}
}