diff --git a/src/Neo/SmartContract/InteropParameterDescriptor.cs b/src/Neo/SmartContract/InteropParameterDescriptor.cs
index 4ddc4d72ef..377d99734f 100644
--- a/src/Neo/SmartContract/InteropParameterDescriptor.cs
+++ b/src/Neo/SmartContract/InteropParameterDescriptor.cs
@@ -64,6 +64,7 @@ public class InteropParameterDescriptor
[typeof(StackItem)] = p => p,
[typeof(Pointer)] = p => p,
[typeof(Array)] = p => p,
+ [typeof(Map)] = p => p,
[typeof(InteropInterface)] = p => p,
[typeof(bool)] = p => p.GetBoolean(),
[typeof(sbyte)] = p => (sbyte)p.GetInteger(),
diff --git a/src/Neo/SmartContract/Native/NFTState.cs b/src/Neo/SmartContract/Native/NFTState.cs
new file mode 100644
index 0000000000..2de564a5c5
--- /dev/null
+++ b/src/Neo/SmartContract/Native/NFTState.cs
@@ -0,0 +1,60 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// NFTState.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Neo.Extensions.IO;
+using Neo.VM;
+using Neo.VM.Types;
+
+namespace Neo.SmartContract.Native;
+
+///
+/// Represents the state of a non-fungible token (NFT), including its asset identifier, owner, and associated properties.
+/// Implements to allow conversion to/from VM .
+///
+public class NFTState : IInteroperable
+{
+ ///
+ /// The asset id (collection) this NFT belongs to.
+ ///
+ public required UInt160 AssetId;
+
+ ///
+ /// The account (owner) that currently owns this NFT.
+ ///
+ public required UInt160 Owner;
+
+ ///
+ /// Arbitrary properties associated with this NFT. Keys are ByteString and values are ByteString or Buffer.
+ ///
+ public required Map Properties;
+
+ ///
+ /// Populates this instance from a VM representation.
+ ///
+ /// A expected to be a with fields in the order: AssetId, Owner, Properties.
+ public void FromStackItem(StackItem stackItem)
+ {
+ Struct @struct = (Struct)stackItem;
+ AssetId = new UInt160(@struct[0].GetSpan());
+ Owner = new UInt160(@struct[1].GetSpan());
+ Properties = (Map)@struct[2];
+ }
+
+ ///
+ /// Convert current NFTState to a VM (Struct).
+ ///
+ /// Optional reference counter used by the VM.
+ /// A representing the NFTState.
+ public StackItem ToStackItem(IReferenceCounter? referenceCounter)
+ {
+ return new Struct(referenceCounter) { AssetId.ToArray(), Owner.ToArray(), Properties };
+ }
+}
diff --git a/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs b/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs
new file mode 100644
index 0000000000..13c59bccbe
--- /dev/null
+++ b/src/Neo/SmartContract/Native/TokenManagement.Fungible.cs
@@ -0,0 +1,153 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// TokenManagement.Fungible.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Neo.VM.Types;
+using System.Numerics;
+
+namespace Neo.SmartContract.Native;
+
+[ContractEvent(1, "Transfer", "assetId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160, "amount", ContractParameterType.Integer)]
+partial class TokenManagement
+{
+ static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128);
+
+ ///
+ /// Creates a new token with an unlimited maximum supply.
+ ///
+ /// The current instance.
+ /// The token name (1-32 characters).
+ /// The token symbol (2-6 characters).
+ /// The number of decimals (0-18).
+ /// The asset identifier generated for the new token.
+ /// If parameter constraints are violated.
+ /// If a token with the same id already exists.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
+ internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals)
+ {
+ return Create(engine, name, symbol, decimals, BigInteger.MinusOne);
+ }
+
+ ///
+ /// Creates a new token with a specified maximum supply.
+ ///
+ /// The current instance.
+ /// The token name (1-32 characters).
+ /// The token symbol (2-6 characters).
+ /// The number of decimals (0-18).
+ /// Maximum total supply, or -1 for unlimited.
+ /// The asset identifier generated for the new token.
+ /// If is less than -1.
+ /// If a token with the same id already exists.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
+ internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals, BigInteger maxSupply)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(maxSupply, BigInteger.MinusOne);
+ UInt160 owner = engine.CallingScriptHash!;
+ UInt160 tokenid = GetAssetId(owner, name);
+ StorageKey key = CreateStorageKey(Prefix_TokenState, tokenid);
+ if (engine.SnapshotCache.Contains(key))
+ throw new InvalidOperationException($"{name} already exists.");
+ var state = new TokenState
+ {
+ Type = TokenType.Fungible,
+ Owner = owner,
+ Name = name,
+ Symbol = symbol,
+ Decimals = decimals,
+ TotalSupply = BigInteger.Zero,
+ MaxSupply = maxSupply
+ };
+ engine.SnapshotCache.Add(key, new(state));
+ Notify(engine, "Created", tokenid, TokenType.Fungible);
+ return tokenid;
+ }
+
+ ///
+ /// Mints new tokens to an account. Only the token owner contract may call this method.
+ ///
+ /// The current instance.
+ /// The asset identifier.
+ /// The recipient account .
+ /// The amount to mint (must be > 0 and <= ).
+ /// A representing the asynchronous operation.
+ /// If is invalid.
+ /// If the asset id does not exist or caller is not the owner or max supply would be exceeded.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
+ internal async Task Mint(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount);
+ AddTotalSupply(engine, TokenType.Fungible, assetId, amount, assertOwner: true);
+ AddBalance(engine.SnapshotCache, assetId, account, amount);
+ await PostTransferAsync(engine, assetId, null, account, amount, StackItem.Null, callOnPayment: true);
+ }
+
+ ///
+ /// Burns tokens from an account, decreasing the total supply. Only the token owner contract may call this method.
+ ///
+ /// The current instance.
+ /// The asset identifier.
+ /// The account from which tokens will be burned.
+ /// The amount to burn (must be > 0 and <= ).
+ /// A representing the asynchronous operation.
+ /// If is invalid.
+ /// If the asset id does not exist, caller is not the owner, or account has insufficient balance.
+ [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.All)]
+ internal async Task Burn(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount);
+ AddTotalSupply(engine, TokenType.Fungible, assetId, -amount, assertOwner: true);
+ if (!AddBalance(engine.SnapshotCache, assetId, account, -amount))
+ throw new InvalidOperationException("Insufficient balance to burn.");
+ await PostTransferAsync(engine, assetId, account, null, amount, StackItem.Null, callOnPayment: false);
+ }
+
+ ///
+ /// Transfers tokens between accounts.
+ ///
+ /// The current instance.
+ /// The asset identifier.
+ /// The sender account .
+ /// The recipient account .
+ /// The amount to transfer (must be >= 0).
+ /// Arbitrary data passed to onPayment or onTransfer callbacks.
+ /// true if the transfer succeeded; otherwise false.
+ /// If is negative.
+ /// If the asset id does not exist.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
+ internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UInt160 from, UInt160 to, BigInteger amount, StackItem data)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(amount);
+ StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
+ TokenState token = engine.SnapshotCache.TryGet(key)?.GetInteroperable()
+ ?? throw new InvalidOperationException("The asset id does not exist.");
+ if (token.Type != TokenType.Fungible)
+ throw new InvalidOperationException("The asset id and the token type do not match.");
+ if (!engine.CheckWitnessInternal(from)) return false;
+ if (!amount.IsZero && from != to)
+ {
+ if (!AddBalance(engine.SnapshotCache, assetId, from, -amount))
+ return false;
+ AddBalance(engine.SnapshotCache, assetId, to, amount);
+ }
+ await PostTransferAsync(engine, assetId, from, to, amount, data, callOnPayment: true);
+ await engine.CallFromNativeContractAsync(Hash, token.Owner, "onTransfer", assetId, from, to, amount, data);
+ return true;
+ }
+
+ async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160 assetId, UInt160? from, UInt160? to, BigInteger amount, StackItem data, bool callOnPayment)
+ {
+ Notify(engine, "Transfer", assetId, from, to, amount);
+ if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return;
+ await engine.CallFromNativeContractAsync(Hash, to, "onPayment", assetId, from, amount, data);
+ }
+}
diff --git a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs
new file mode 100644
index 0000000000..e08e346861
--- /dev/null
+++ b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs
@@ -0,0 +1,289 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// TokenManagement.NonFungible.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Neo.Persistence;
+using Neo.SmartContract.Iterators;
+using Neo.VM.Types;
+using System.Numerics;
+
+namespace Neo.SmartContract.Native;
+
+[ContractEvent(2, "NFTTransfer", "uniqueId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160)]
+partial class TokenManagement
+{
+ const byte Prefix_NFTUniqueIdSeed = 15;
+ const byte Prefix_NFTState = 8;
+ const byte Prefix_NFTOwnerUniqueIdIndex = 21;
+ const byte Prefix_NFTAssetIdUniqueIdIndex = 23;
+
+ partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork)
+ {
+ if (hardfork == ActiveIn)
+ {
+ engine.SnapshotCache.Add(CreateStorageKey(Prefix_NFTUniqueIdSeed), BigInteger.Zero);
+ }
+ }
+
+ ///
+ /// Creates a new NFT collection with an unlimited maximum supply.
+ ///
+ /// The current instance.
+ /// The NFT collection name (1-32 characters).
+ /// The NFT collection symbol (2-6 characters).
+ /// The asset identifier generated for the new NFT collection.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
+ internal UInt160 CreateNonFungible(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol)
+ {
+ return CreateNonFungible(engine, name, symbol, BigInteger.MinusOne);
+ }
+
+ ///
+ /// Creates a new NFT collection with a specified maximum supply.
+ ///
+ /// The current instance.
+ /// The NFT collection name (1-32 characters).
+ /// The NFT collection symbol (2-6 characters).
+ /// Maximum total supply for NFTs in this collection, or -1 for unlimited.
+ /// The asset identifier generated for the new NFT collection.
+ /// If is less than -1.
+ /// If a collection with the same id already exists.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
+ internal UInt160 CreateNonFungible(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, BigInteger maxSupply)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(maxSupply, BigInteger.MinusOne);
+ UInt160 owner = engine.CallingScriptHash!;
+ UInt160 tokenid = GetAssetId(owner, name);
+ StorageKey key = CreateStorageKey(Prefix_TokenState, tokenid);
+ if (engine.SnapshotCache.Contains(key))
+ throw new InvalidOperationException($"{name} already exists.");
+ var state = new TokenState
+ {
+ Type = TokenType.NonFungible,
+ Owner = owner,
+ Name = name,
+ Symbol = symbol,
+ Decimals = 0,
+ TotalSupply = BigInteger.Zero,
+ MaxSupply = maxSupply
+ };
+ engine.SnapshotCache.Add(key, new(state));
+ Notify(engine, "Created", tokenid, TokenType.NonFungible);
+ return tokenid;
+ }
+
+ ///
+ /// Mints a new NFT for the given collection to the specified account using empty properties.
+ ///
+ /// The current instance.
+ /// The NFT collection asset identifier.
+ /// The recipient account .
+ /// The unique id () of the newly minted NFT.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
+ internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, UInt160 account)
+ {
+ return await MintNFT(engine, assetId, account, new Map(engine.ReferenceCounter));
+ }
+
+ ///
+ /// Mints a new NFT for the given collection to the specified account with provided properties.
+ ///
+ /// The current instance.
+ /// The NFT collection asset identifier.
+ /// The recipient account .
+ /// A of properties for the NFT (keys: ByteString, values: ByteString or Buffer).
+ /// The unique id () of the newly minted NFT.
+ /// If properties are invalid (too many, invalid key/value types or lengths).
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 10, RequiredCallFlags = CallFlags.All)]
+ internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, UInt160 account, Map properties)
+ {
+ if (properties.Count > 8)
+ throw new ArgumentException("Too many properties.", nameof(properties));
+ foreach (var (k, v) in properties)
+ {
+ if (k is not ByteString)
+ throw new ArgumentException("The key of a property should be a ByteString.", nameof(properties));
+ if (k.Size < 1 || k.Size > 16)
+ throw new ArgumentException("The key length of a property should be between 1 and 16.", nameof(properties));
+ k.GetString(); // Ensure to invoke `ToStrictUtf8String()`
+ switch (v)
+ {
+ case ByteString bs:
+ if (bs.Size < 1 || bs.Size > 128)
+ throw new ArgumentException("The value length of a property should be between 1 and 128.", nameof(properties));
+ break;
+ case VM.Types.Buffer buffer:
+ if (buffer.Size < 1 || buffer.Size > 128)
+ throw new ArgumentException("The value length of a property should be between 1 and 128.", nameof(properties));
+ break;
+ default:
+ throw new ArgumentException("The value of a property should be a ByteString or Buffer.", nameof(properties));
+ }
+ v.GetString(); // Ensure to invoke `ToStrictUtf8String()`
+ }
+ AddTotalSupply(engine, TokenType.NonFungible, assetId, 1, assertOwner: true);
+ AddBalance(engine.SnapshotCache, assetId, account, 1);
+ UInt160 uniqueId = GetNextNFTUniqueId(engine);
+ StorageKey key = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, assetId, uniqueId);
+ engine.SnapshotCache.Add(key, new());
+ key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account, uniqueId);
+ engine.SnapshotCache.Add(key, new());
+ key = CreateStorageKey(Prefix_NFTState, uniqueId);
+ engine.SnapshotCache.Add(key, new(new NFTState
+ {
+ AssetId = assetId,
+ Owner = account,
+ Properties = (Map)properties.DeepCopy(asImmutable: true)
+ }));
+ await PostNFTTransferAsync(engine, uniqueId, null, account, StackItem.Null, callOnPayment: true);
+ return uniqueId;
+ }
+
+ ///
+ /// Burns an NFT identified by . Only the owner contract may call this method.
+ ///
+ /// The current instance.
+ /// The unique id of the NFT to burn.
+ /// A representing the asynchronous operation.
+ /// If the unique id does not exist or owner has insufficient balance or caller is not owner contract.
+ [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.All)]
+ internal async Task BurnNFT(ApplicationEngine engine, UInt160 uniqueId)
+ {
+ StorageKey key = CreateStorageKey(Prefix_NFTState, uniqueId);
+ NFTState nft = engine.SnapshotCache.TryGet(key)?.GetInteroperable()
+ ?? throw new InvalidOperationException("The unique id does not exist.");
+ AddTotalSupply(engine, TokenType.NonFungible, nft.AssetId, BigInteger.MinusOne, assertOwner: true);
+ if (!AddBalance(engine.SnapshotCache, nft.AssetId, nft.Owner, BigInteger.MinusOne))
+ throw new InvalidOperationException("Insufficient balance to burn.");
+ engine.SnapshotCache.Delete(key);
+ key = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, nft.AssetId, uniqueId);
+ engine.SnapshotCache.Delete(key);
+ key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, nft.Owner, uniqueId);
+ engine.SnapshotCache.Delete(key);
+ await PostNFTTransferAsync(engine, uniqueId, nft.Owner, null, StackItem.Null, callOnPayment: false);
+ }
+
+ ///
+ /// Transfers an NFT between owners.
+ ///
+ /// The current instance.
+ /// The unique id of the NFT.
+ /// The current owner account .
+ /// The recipient account .
+ /// Arbitrary data passed to onNFTPayment or onNFTTransfer callbacks.
+ /// true if the transfer succeeded; otherwise false.
+ /// If the unique id does not exist.
+ [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
+ internal async Task TransferNFT(ApplicationEngine engine, UInt160 uniqueId, UInt160 from, UInt160 to, StackItem data)
+ {
+ StorageKey key_nft = CreateStorageKey(Prefix_NFTState, uniqueId);
+ NFTState nft = engine.SnapshotCache.TryGet(key_nft)?.GetInteroperable()
+ ?? throw new InvalidOperationException("The unique id does not exist.");
+ if (nft.Owner != from) return false;
+ if (!engine.CheckWitnessInternal(from)) return false;
+ StorageKey key = CreateStorageKey(Prefix_TokenState, nft.AssetId);
+ TokenState token = engine.SnapshotCache.TryGet(key)!.GetInteroperable();
+ if (from != to)
+ {
+ if (!AddBalance(engine.SnapshotCache, nft.AssetId, from, BigInteger.MinusOne))
+ return false;
+ AddBalance(engine.SnapshotCache, nft.AssetId, to, BigInteger.One);
+ key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, from, uniqueId);
+ engine.SnapshotCache.Delete(key);
+ key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, to, uniqueId);
+ engine.SnapshotCache.Add(key, new());
+ nft = engine.SnapshotCache.GetAndChange(key_nft)!.GetInteroperable();
+ nft.Owner = to;
+ }
+ await PostNFTTransferAsync(engine, uniqueId, from, to, data, callOnPayment: true);
+ await engine.CallFromNativeContractAsync(Hash, token.Owner, "onNFTTransfer", uniqueId, from, to, data);
+ return true;
+ }
+
+ ///
+ /// Gets NFT metadata for a unique id.
+ ///
+ /// A readonly view of the storage.
+ /// The unique id of the NFT.
+ /// The if found; otherwise null.
+ [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
+ public NFTState? GetNFTInfo(IReadOnlyStore snapshot, UInt160 uniqueId)
+ {
+ StorageKey key = CreateStorageKey(Prefix_NFTState, uniqueId);
+ return snapshot.TryGet(key)?.GetInteroperable();
+ }
+
+ ///
+ /// Returns an iterator over the unique ids of NFTs for the specified asset (collection).
+ /// The iterator yields the stored unique id keys (UInt160) indexed under the NFT asset id.
+ ///
+ /// A readonly view of the storage.
+ /// The asset (collection) identifier whose NFTs are requested.
+ ///
+ /// An that enumerates the NFT unique ids belonging to the given collection.
+ /// The iterator is configured to return keys only and to remove the storage prefix.
+ ///
+ /// Thrown when the specified asset id does not exist.
+ ///
+ /// The returned iterator is backed by the storage layer and uses the NFT asset-to-unique-id index.
+ /// Consumers should dispose the iterator when finished if they hold unmanaged resources from it.
+ ///
+ [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)]
+ public IIterator GetNFTs(IReadOnlyStore snapshot, UInt160 assetId)
+ {
+ StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
+ if (!snapshot.Contains(key))
+ throw new InvalidOperationException("The asset id does not exist.");
+ const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix;
+ var prefixKey = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, assetId);
+ var enumerator = snapshot.Find(prefixKey).GetEnumerator();
+ return new StorageIterator(enumerator, 21, options);
+ }
+
+ ///
+ /// Returns an iterator over the unique ids of NFTs owned by the specified account.
+ /// The iterator yields the stored unique id keys () indexed under the NFT owner index.
+ ///
+ /// A readonly view of the storage.
+ /// The account whose NFTs are requested.
+ ///
+ /// An that enumerates the NFT unique ids owned by the given account.
+ /// The iterator is configured to return keys only and to remove the storage prefix.
+ ///
+ ///
+ /// The returned iterator is backed by the storage layer and uses the NFT owner-to-unique-id index.
+ /// Consumers should dispose the iterator when finished if they hold unmanaged resources from it.
+ ///
+ [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)]
+ public IIterator GetNFTsOfOwner(IReadOnlyStore snapshot, UInt160 account)
+ {
+ const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix;
+ var prefixKey = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account);
+ var enumerator = snapshot.Find(prefixKey).GetEnumerator();
+ return new StorageIterator(enumerator, 21, options);
+ }
+
+ UInt160 GetNextNFTUniqueId(ApplicationEngine engine)
+ {
+ StorageKey key = CreateStorageKey(Prefix_NFTUniqueIdSeed);
+ BigInteger seed = engine.SnapshotCache.GetAndChange(key)!.Add(BigInteger.One);
+ using MemoryStream ms = new();
+ ms.Write(engine.PersistingBlock!.Hash.GetSpan());
+ ms.Write(seed.ToByteArrayStandard());
+ return ms.ToArray().ToScriptHash();
+ }
+
+ async ContractTask PostNFTTransferAsync(ApplicationEngine engine, UInt160 uniqueId, UInt160? from, UInt160? to, StackItem data, bool callOnPayment)
+ {
+ Notify(engine, "NFTTransfer", uniqueId, from, to);
+ if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return;
+ await engine.CallFromNativeContractAsync(Hash, to, "onNFTPayment", uniqueId, from, data);
+ }
+}
diff --git a/src/Neo/SmartContract/Native/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs
index 378eaa3dc4..1f75815176 100644
--- a/src/Neo/SmartContract/Native/TokenManagement.cs
+++ b/src/Neo/SmartContract/Native/TokenManagement.cs
@@ -9,10 +9,7 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.
-using Neo.Extensions.IO;
using Neo.Persistence;
-using Neo.VM;
-using Neo.VM.Types;
using System.Numerics;
namespace Neo.SmartContract.Native;
@@ -20,150 +17,35 @@ namespace Neo.SmartContract.Native;
///
/// Provides core functionality for creating, managing, and transferring tokens within a native contract environment.
///
-[ContractEvent(0, "Created", "assetId", ContractParameterType.Hash160)]
-[ContractEvent(1, "Transfer", "assetId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160, "amount", ContractParameterType.Integer)]
-public sealed class TokenManagement : NativeContract
+[ContractEvent(0, "Created", "assetId", ContractParameterType.Hash160, "type", ContractParameterType.Integer)]
+public sealed partial class TokenManagement : NativeContract
{
const byte Prefix_TokenState = 10;
const byte Prefix_AccountState = 12;
- static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128);
-
internal TokenManagement() : base(-12) { }
- ///
- /// Creates a new token with an unlimited maximum supply.
- ///
- /// The current instance.
- /// The token name (1-32 characters).
- /// The token symbol (2-6 characters).
- /// The number of decimals (0-18).
- /// The asset identifier generated for the new token.
- /// If parameter constraints are violated.
- /// If a token with the same id already exists.
- [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
- internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals)
- {
- return Create(engine, name, symbol, decimals, BigInteger.MinusOne);
- }
+ partial void Initialize_Fungible(ApplicationEngine engine, Hardfork? hardfork);
+ partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork);
- ///
- /// Creates a new token with a specified maximum supply.
- ///
- /// The current instance.
- /// The token name (1-32 characters).
- /// The token symbol (2-6 characters).
- /// The number of decimals (0-18).
- /// Maximum total supply, or -1 for unlimited.
- /// The asset identifier generated for the new token.
- /// If is less than -1.
- /// If a token with the same id already exists.
- [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
- internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, [Range(0, 18)] byte decimals, BigInteger maxSupply)
+ internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork)
{
- ArgumentOutOfRangeException.ThrowIfLessThan(maxSupply, BigInteger.MinusOne);
- UInt160 owner = engine.CallingScriptHash!;
- UInt160 tokenid = GetAssetId(owner, name);
- StorageKey key = CreateStorageKey(Prefix_TokenState, tokenid);
- if (engine.SnapshotCache.Contains(key))
- throw new InvalidOperationException($"{name} already exists.");
- var state = new TokenState
- {
- Owner = owner,
- Name = name,
- Symbol = symbol,
- Decimals = decimals,
- TotalSupply = BigInteger.Zero,
- MaxSupply = maxSupply
- };
- engine.SnapshotCache.Add(key, new(state));
- Notify(engine, "Created", tokenid);
- return tokenid;
+ Initialize_Fungible(engine, hardfork);
+ Initialize_NonFungible(engine, hardfork);
+ return ContractTask.CompletedTask;
}
///
/// Retrieves the token metadata for the given asset id.
///
- /// The current instance.
+ /// A readonly view of the storage.
/// The asset identifier.
/// The if found; otherwise null.
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
- public TokenState? GetTokenInfo(ApplicationEngine engine, UInt160 assetId)
+ public TokenState? GetTokenInfo(IReadOnlyStore snapshot, UInt160 assetId)
{
StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
- return engine.SnapshotCache.TryGet(key)?.GetInteroperable();
- }
-
- ///
- /// Mints new tokens to an account. Only the token owner contract may call this method.
- ///
- /// The current instance.
- /// The asset identifier.
- /// The recipient account .
- /// The amount to mint (must be > 0 and <= ).
- /// A representing the asynchronous operation.
- /// If is invalid.
- /// If the asset id does not exist or caller is not the owner or max supply would be exceeded.
- [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
- internal async Task Mint(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount)
- {
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
- ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount);
- AddTotalSupply(engine, assetId, amount, assertOwner: true);
- AddBalance(engine.SnapshotCache, assetId, account, amount);
- await PostTransferAsync(engine, assetId, null, account, amount, StackItem.Null, callOnPayment: true);
- }
-
- ///
- /// Burns tokens from an account, decreasing the total supply. Only the token owner contract may call this method.
- ///
- /// The current instance.
- /// The asset identifier.
- /// The account from which tokens will be burned.
- /// The amount to burn (must be > 0 and <= ).
- /// A representing the asynchronous operation.
- /// If is invalid.
- /// If the asset id does not exist, caller is not the owner, or account has insufficient balance.
- [ContractMethod(CpuFee = 1 << 17, RequiredCallFlags = CallFlags.All)]
- internal async Task Burn(ApplicationEngine engine, UInt160 assetId, UInt160 account, BigInteger amount)
- {
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
- ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount);
- AddTotalSupply(engine, assetId, -amount, assertOwner: true);
- if (!AddBalance(engine.SnapshotCache, assetId, account, -amount))
- throw new InvalidOperationException("Insufficient balance to burn.");
- await PostTransferAsync(engine, assetId, account, null, amount, StackItem.Null, callOnPayment: false);
- }
-
- ///
- /// Transfers tokens between accounts.
- ///
- /// The current instance.
- /// The asset identifier.
- /// The sender account .
- /// The recipient account .
- /// The amount to transfer (must be >= 0).
- /// Arbitrary data passed to onPayment or onTransfer callbacks.
- /// true if the transfer succeeded; otherwise false.
- /// If is negative.
- /// If the asset id does not exist.
- [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
- internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UInt160 from, UInt160 to, BigInteger amount, StackItem data)
- {
- ArgumentOutOfRangeException.ThrowIfNegative(amount);
- StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
- TokenState token = engine.SnapshotCache.TryGet(key)?.GetInteroperable()
- ?? throw new InvalidOperationException("The asset id does not exist.");
- if (!engine.CheckWitnessInternal(from)) return false;
- if (!amount.IsZero && from != to)
- {
- if (!AddBalance(engine.SnapshotCache, assetId, from, -amount))
- return false;
- AddBalance(engine.SnapshotCache, assetId, to, amount);
- }
- await PostTransferAsync(engine, assetId, from, to, amount, data, callOnPayment: true);
- await engine.CallFromNativeContractAsync(Hash, token.Owner, "onTransfer", assetId, from, to, amount, data);
- return true;
+ return snapshot.TryGet(key)?.GetInteroperable();
}
///
@@ -201,11 +83,13 @@ public static UInt160 GetAssetId(UInt160 owner, string name)
return buffer.ToScriptHash();
}
- void AddTotalSupply(ApplicationEngine engine, UInt160 assetId, BigInteger amount, bool assertOwner)
+ void AddTotalSupply(ApplicationEngine engine, TokenType type, UInt160 assetId, BigInteger amount, bool assertOwner)
{
StorageKey key = CreateStorageKey(Prefix_TokenState, assetId);
TokenState token = engine.SnapshotCache.GetAndChange(key)?.GetInteroperable()
?? throw new InvalidOperationException("The asset id does not exist.");
+ if (token.Type != type)
+ throw new InvalidOperationException("The asset id and the token type do not match.");
if (assertOwner && token.Owner != engine.CallingScriptHash)
throw new InvalidOperationException("This method can be called by the owner contract only.");
token.TotalSupply += amount;
@@ -242,73 +126,4 @@ bool AddBalance(DataCache snapshot, UInt160 assetId, UInt160 account, BigInteger
}
return true;
}
-
- async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160 assetId, UInt160? from, UInt160? to, BigInteger amount, StackItem data, bool callOnPayment)
- {
- Notify(engine, "Transfer", assetId, from, to, amount);
- if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return;
- await engine.CallFromNativeContractAsync(Hash, to, "onPayment", assetId, from, amount, data);
- }
-}
-
-///
-/// Represents the persisted metadata for a token.
-/// Implements to allow conversion to/from VM .
-///
-public class TokenState : IInteroperable
-{
- ///
- /// The owner contract script hash that can manage this token (mint/burn, onTransfer callback target).
- ///
- public required UInt160 Owner;
-
- ///
- /// The token's human-readable name.
- ///
- public required string Name;
-
- ///
- /// The token's symbol (short string).
- ///
- public required string Symbol;
-
- ///
- /// Number of decimal places the token supports.
- ///
- public required byte Decimals;
-
- ///
- /// Current total supply of the token.
- ///
- public BigInteger TotalSupply;
-
- ///
- /// Maximum total supply allowed; -1 indicates no limit.
- ///
- public BigInteger MaxSupply;
-
- ///
- /// Populates this instance from a VM representation.
- ///
- /// A expected to be a with the token fields in order.
- public void FromStackItem(StackItem stackItem)
- {
- Struct @struct = (Struct)stackItem;
- Owner = new UInt160(@struct[0].GetSpan());
- Name = @struct[1].GetString()!;
- Symbol = @struct[2].GetString()!;
- Decimals = (byte)@struct[3].GetInteger();
- TotalSupply = @struct[4].GetInteger();
- MaxSupply = @struct[5].GetInteger();
- }
-
- ///
- /// Converts this instance to a VM representation.
- ///
- /// Optional reference counter used by the VM.
- /// A containing the token fields in order.
- public StackItem ToStackItem(IReferenceCounter? referenceCounter)
- {
- return new Struct(referenceCounter) { Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply };
- }
}
diff --git a/src/Neo/SmartContract/Native/TokenState.cs b/src/Neo/SmartContract/Native/TokenState.cs
new file mode 100644
index 0000000000..de6d21946e
--- /dev/null
+++ b/src/Neo/SmartContract/Native/TokenState.cs
@@ -0,0 +1,85 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// TokenState.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Neo.Extensions.IO;
+using Neo.VM;
+using Neo.VM.Types;
+using System.Numerics;
+
+namespace Neo.SmartContract.Native;
+
+///
+/// Represents the persisted metadata for a token.
+/// Implements to allow conversion to/from VM .
+///
+public class TokenState : IInteroperable
+{
+ ///
+ /// Specifies the type of token represented by this instance.
+ ///
+ public required TokenType Type;
+
+ ///
+ /// The owner contract script hash that can manage this token (mint/burn, onTransfer callback target).
+ ///
+ public required UInt160 Owner;
+
+ ///
+ /// The token's human-readable name.
+ ///
+ public required string Name;
+
+ ///
+ /// The token's symbol (short string).
+ ///
+ public required string Symbol;
+
+ ///
+ /// Number of decimal places the token supports.
+ ///
+ public required byte Decimals;
+
+ ///
+ /// Current total supply of the token.
+ ///
+ public BigInteger TotalSupply;
+
+ ///
+ /// Maximum total supply allowed; -1 indicates no limit.
+ ///
+ public BigInteger MaxSupply;
+
+ ///
+ /// Populates this instance from a VM representation.
+ ///
+ /// A expected to be a with the token fields in order.
+ public void FromStackItem(StackItem stackItem)
+ {
+ Struct @struct = (Struct)stackItem;
+ Type = (TokenType)(byte)@struct[0].GetInteger();
+ Owner = new UInt160(@struct[1].GetSpan());
+ Name = @struct[2].GetString()!;
+ Symbol = @struct[3].GetString()!;
+ Decimals = (byte)@struct[4].GetInteger();
+ TotalSupply = @struct[5].GetInteger();
+ MaxSupply = @struct[6].GetInteger();
+ }
+
+ ///
+ /// Converts this instance to a VM representation.
+ ///
+ /// Optional reference counter used by the VM.
+ /// A containing the token fields in order.
+ public StackItem ToStackItem(IReferenceCounter? referenceCounter)
+ {
+ return new Struct(referenceCounter) { (byte)Type, Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply };
+ }
+}
diff --git a/src/Neo/SmartContract/Native/TokenType.cs b/src/Neo/SmartContract/Native/TokenType.cs
new file mode 100644
index 0000000000..5dc13167a2
--- /dev/null
+++ b/src/Neo/SmartContract/Native/TokenType.cs
@@ -0,0 +1,27 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// TokenType.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+namespace Neo.SmartContract.Native;
+
+///
+/// Specifies the type of token, indicating whether it is fungible or non-fungible.
+///
+public enum TokenType : byte
+{
+ ///
+ /// Fungible token type.
+ ///
+ Fungible = 1,
+ ///
+ /// Non-fungible token (NFT) type.
+ ///
+ NonFungible = 2
+}
diff --git a/src/Neo/SmartContract/StorageItem.cs b/src/Neo/SmartContract/StorageItem.cs
index 7de3c70214..5d88d4d32d 100644
--- a/src/Neo/SmartContract/StorageItem.cs
+++ b/src/Neo/SmartContract/StorageItem.cs
@@ -137,9 +137,11 @@ public void Seal()
/// Increases the integer value in the store by the specified value.
///
/// The integer to add.
- public void Add(BigInteger integer)
+ public BigInteger Add(BigInteger integer)
{
- Set(this + integer);
+ BigInteger result = this + integer;
+ Set(result);
+ return result;
}
///
diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
index 200cce7327..248c201f6c 100644
--- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
+++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs
@@ -47,7 +47,7 @@ public void TestSetup()
{"OracleContract", """{"id":-9,"updatecounter":0,"hash":"0xfe924b7cfe89ddd271abaf7210a80a7e11178758","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"OracleContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"finish","parameters":[],"returntype":"Void","offset":0,"safe":false},{"name":"getPrice","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"request","parameters":[{"name":"url","type":"String"},{"name":"filter","type":"String"},{"name":"callback","type":"String"},{"name":"userData","type":"Any"},{"name":"gasForResponse","type":"Integer"}],"returntype":"Void","offset":14,"safe":false},{"name":"setPrice","parameters":[{"name":"price","type":"Integer"}],"returntype":"Void","offset":21,"safe":false},{"name":"verify","parameters":[],"returntype":"Boolean","offset":28,"safe":true}],"events":[{"name":"OracleRequest","parameters":[{"name":"Id","type":"Integer"},{"name":"RequestContract","type":"Hash160"},{"name":"Url","type":"String"},{"name":"Filter","type":"String"}]},{"name":"OracleResponse","parameters":[{"name":"Id","type":"Integer"},{"name":"OriginalTx","type":"Hash256"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"Notary", """{"id":-10,"updatecounter":0,"hash":"0xc1e14f19c3e60d0b9244d06dd7ba9b113135ec3b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"Notary","groups":[],"features":{},"supportedstandards":["NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"expirationOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":7,"safe":true},{"name":"getMaxNotValidBeforeDelta","parameters":[],"returntype":"Integer","offset":14,"safe":true},{"name":"lockDepositUntil","parameters":[{"name":"account","type":"Hash160"},{"name":"till","type":"Integer"}],"returntype":"Boolean","offset":21,"safe":false},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":28,"safe":false},{"name":"setMaxNotValidBeforeDelta","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":35,"safe":false},{"name":"verify","parameters":[{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":42,"safe":true},{"name":"withdraw","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}],"returntype":"Boolean","offset":49,"safe":false}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
{"Treasury", """{"id":-11,"updatecounter":0,"hash":"0x156326f25b1b5d839a4d326aeaa75383c9563ac1","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1592866325},"manifest":{"name":"Treasury","groups":[],"features":{},"supportedstandards":["NEP-26","NEP-27"],"abi":{"methods":[{"name":"onNEP11Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"tokenId","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":0,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":7,"safe":true},{"name":"verify","parameters":[],"returntype":"Boolean","offset":14,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""},
- {"TokenManagement", """{"id":-12,"updatecounter":0,"hash":"0xae00c57daeb20f9b6545f65a018f44a8a40e049f","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":4064424832},"manifest":{"name":"TokenManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"burn","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":7,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"}],"returntype":"Hash160","offset":14,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"},{"name":"maxSupply","type":"Integer"}],"returntype":"Hash160","offset":21,"safe":false},{"name":"getTokenInfo","parameters":[{"name":"assetId","type":"Hash160"}],"returntype":"Array","offset":28,"safe":true},{"name":"mint","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":35,"safe":false},{"name":"transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"InteropInterface","offset":42,"safe":false}],"events":[{"name":"Created","parameters":[{"name":"assetId","type":"Hash160"}]},{"name":"Transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" }
+ {"TokenManagement", """{"id":-12,"updatecounter":0,"hash":"0xae00c57daeb20f9b6545f65a018f44a8a40e049f","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":1841570703},"manifest":{"name":"TokenManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"burn","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":7,"safe":false},{"name":"burnNFT","parameters":[{"name":"uniqueId","type":"Hash160"}],"returntype":"InteropInterface","offset":14,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"}],"returntype":"Hash160","offset":21,"safe":false},{"name":"create","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"decimals","type":"Integer"},{"name":"maxSupply","type":"Integer"}],"returntype":"Hash160","offset":28,"safe":false},{"name":"createNonFungible","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNonFungible","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"},{"name":"maxSupply","type":"Integer"}],"returntype":"Hash160","offset":42,"safe":false},{"name":"getNFTInfo","parameters":[{"name":"uniqueId","type":"Hash160"}],"returntype":"Array","offset":49,"safe":true},{"name":"getNFTs","parameters":[{"name":"assetId","type":"Hash160"}],"returntype":"InteropInterface","offset":56,"safe":true},{"name":"getNFTsOfOwner","parameters":[{"name":"account","type":"Hash160"}],"returntype":"InteropInterface","offset":63,"safe":true},{"name":"getTokenInfo","parameters":[{"name":"assetId","type":"Hash160"}],"returntype":"Array","offset":70,"safe":true},{"name":"mint","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"amount","type":"Integer"}],"returntype":"InteropInterface","offset":77,"safe":false},{"name":"mintNFT","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"}],"returntype":"InteropInterface","offset":84,"safe":false},{"name":"mintNFT","parameters":[{"name":"assetId","type":"Hash160"},{"name":"account","type":"Hash160"},{"name":"properties","type":"Map"}],"returntype":"InteropInterface","offset":91,"safe":false},{"name":"transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"InteropInterface","offset":98,"safe":false},{"name":"transferNFT","parameters":[{"name":"uniqueId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"data","type":"Any"}],"returntype":"InteropInterface","offset":105,"safe":false}],"events":[{"name":"Created","parameters":[{"name":"assetId","type":"Hash160"},{"name":"type","type":"Integer"}]},{"name":"Transfer","parameters":[{"name":"assetId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"NFTTransfer","parameters":[{"name":"uniqueId","type":"Hash160"},{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" }
};
}