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}}""" } }; }