From a6f9006583b440f8fc417e73de8e144bf2c32c07 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Fri, 5 Dec 2025 22:53:55 +0800 Subject: [PATCH 01/10] Add NFT to TokenManagement --- .../InteropParameterDescriptor.cs | 1 + .../SmartContract/Native/TokenManagement.cs | 226 +++++++++++++++++- src/Neo/SmartContract/StorageItem.cs | 7 +- 3 files changed, 223 insertions(+), 11 deletions(-) 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/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs index 378eaa3dc4..c7b470bd4a 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -11,6 +11,7 @@ using Neo.Extensions.IO; using Neo.Persistence; +using Neo.SmartContract.Iterators; using Neo.VM; using Neo.VM.Types; using System.Numerics; @@ -20,17 +21,30 @@ 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(0, "Created", "assetId", ContractParameterType.Hash160, "type", ContractParameterType.Integer)] [ContractEvent(1, "Transfer", "assetId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160, "amount", ContractParameterType.Integer)] +[ContractEvent(1, "NFTTransfer", "uniqueId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160)] public sealed class TokenManagement : NativeContract { const byte Prefix_TokenState = 10; const byte Prefix_AccountState = 12; + const byte Prefix_NFTUniqueIdSeed = 15; + const byte Prefix_NFTAccountToUniqueId = 20; + const byte Prefix_NFTState = 8; static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128); internal TokenManagement() : base(-12) { } + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) + { + if (hardfork == ActiveIn) + { + engine.SnapshotCache.Add(CreateStorageKey(Prefix_NFTUniqueIdSeed), BigInteger.Zero); + } + return ContractTask.CompletedTask; + } + /// /// Creates a new token with an unlimited maximum supply. /// @@ -69,6 +83,7 @@ internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [ throw new InvalidOperationException($"{name} already exists."); var state = new TokenState { + Type = TokenType.Fungible, Owner = owner, Name = name, Symbol = symbol, @@ -77,21 +92,51 @@ internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [ MaxSupply = maxSupply }; engine.SnapshotCache.Add(key, new(state)); - Notify(engine, "Created", tokenid); + Notify(engine, "Created", tokenid, TokenType.Fungible); + return tokenid; + } + + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) + { + return CreateNFT(engine, name, symbol, BigInteger.MinusOne); + } + + [ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)] + internal UInt160 CreateNFT(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; } /// /// 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(); + return snapshot.TryGet(key)?.GetInteroperable(); } /// @@ -109,11 +154,60 @@ internal async Task Mint(ApplicationEngine engine, UInt160 assetId, UInt160 acco { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount); - AddTotalSupply(engine, assetId, amount, assertOwner: true); + 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); } + [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)); + } + + [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_NFTAccountToUniqueId, account, uniqueId); + engine.SnapshotCache.Add(key, new()); + key = CreateStorageKey(Prefix_NFTState, uniqueId); + engine.SnapshotCache.Add(key, new(new NFTState + { + AssetId = assetId, + Owner = account, + Properties = properties + })); + await PostNFTTransferAsync(engine, uniqueId, null, account, StackItem.Null, callOnPayment: true); + return uniqueId; + } + /// /// Burns tokens from an account, decreasing the total supply. Only the token owner contract may call this method. /// @@ -129,12 +223,27 @@ internal async Task Burn(ApplicationEngine engine, UInt160 assetId, UInt160 acco { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount); ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, MaxMintAmount); - AddTotalSupply(engine, assetId, -amount, assertOwner: true); + 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); } + [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_NFTAccountToUniqueId, nft.Owner, uniqueId); + engine.SnapshotCache.Delete(key); + await PostNFTTransferAsync(engine, uniqueId, nft.Owner, null, StackItem.Null, callOnPayment: false); + } + /// /// Transfers tokens between accounts. /// @@ -154,6 +263,8 @@ internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UI 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) { @@ -166,6 +277,33 @@ internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UI return true; } + [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_NFTAccountToUniqueId, from, uniqueId); + engine.SnapshotCache.Delete(key); + key = CreateStorageKey(Prefix_NFTAccountToUniqueId, 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; + } + /// /// Returns the balance of for the specified . /// @@ -186,6 +324,31 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac return accountState.Balance; } + [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(); + } + + [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] + public IIterator GetNFTs(IReadOnlyStore snapshot) + { + const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix; + var prefixKey = CreateStorageKey(Prefix_NFTState); + var enumerator = snapshot.Find(prefixKey).GetEnumerator(); + return new StorageIterator(enumerator, 1, options); + } + + [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_NFTAccountToUniqueId, account); + var enumerator = snapshot.Find(prefixKey).GetEnumerator(); + return new StorageIterator(enumerator, 21, options); + } + /// /// Computes a unique asset id from the token owner's script hash and the token name. /// @@ -201,11 +364,23 @@ public static UInt160 GetAssetId(UInt160 owner, string name) return buffer.ToScriptHash(); } - void AddTotalSupply(ApplicationEngine engine, UInt160 assetId, BigInteger amount, bool assertOwner) + 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(); + } + + 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; @@ -249,6 +424,19 @@ async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160 assetId, if (!callOnPayment || to is null || !ContractManagement.IsContract(engine.SnapshotCache, to)) return; await engine.CallFromNativeContractAsync(Hash, to, "onPayment", assetId, from, amount, data); } + + 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); + } +} + +public enum TokenType : byte +{ + Fungible = 1, + NonFungible = 2 } /// @@ -257,6 +445,8 @@ async ContractTask PostTransferAsync(ApplicationEngine engine, UInt160 assetId, /// public class TokenState : IInteroperable { + public required TokenType Type; + /// /// The owner contract script hash that can manage this token (mint/burn, onTransfer callback target). /// @@ -312,3 +502,23 @@ public StackItem ToStackItem(IReferenceCounter? referenceCounter) return new Struct(referenceCounter) { Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply }; } } + +public class NFTState : IInteroperable +{ + public required UInt160 AssetId; + public required UInt160 Owner; + public required Map 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]; + } + + public StackItem ToStackItem(IReferenceCounter? referenceCounter) + { + return new Struct(referenceCounter) { AssetId.ToArray(), Owner.ToArray(), Properties }; + } +} diff --git a/src/Neo/SmartContract/StorageItem.cs b/src/Neo/SmartContract/StorageItem.cs index 7de3c70214..c20ff4def3 100644 --- a/src/Neo/SmartContract/StorageItem.cs +++ b/src/Neo/SmartContract/StorageItem.cs @@ -137,9 +137,9 @@ 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); + return Set(this + integer); } /// @@ -277,10 +277,11 @@ public void Serialize(BinaryWriter writer) /// Sets the integer value of the storage. /// /// The integer value to set. - public void Set(BigInteger integer) + public BigInteger Set(BigInteger integer) { _cache = integer; _value = null; + return integer; } public static implicit operator BigInteger(StorageItem item) From eba757b80f446cba8d87f21d514d22cb51a8e2ab Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Fri, 5 Dec 2025 23:03:37 +0800 Subject: [PATCH 02/10] Fix UT --- tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 200cce7327..2f7f191d6b 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":"createNFT","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNFT","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":[],"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}}""" } }; } From 17cf180981791ba9455c70fe08a1d0f4af76c616 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Fri, 5 Dec 2025 23:31:01 +0800 Subject: [PATCH 03/10] Rename --- src/Neo/SmartContract/Native/TokenManagement.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Neo/SmartContract/Native/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs index c7b470bd4a..df9c73ed71 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -29,8 +29,8 @@ public sealed class TokenManagement : NativeContract const byte Prefix_TokenState = 10; const byte Prefix_AccountState = 12; const byte Prefix_NFTUniqueIdSeed = 15; - const byte Prefix_NFTAccountToUniqueId = 20; const byte Prefix_NFTState = 8; + const byte Prefix_NFTOwnerUniqueIdIndex = 21; static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128); @@ -195,7 +195,7 @@ internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, AddTotalSupply(engine, TokenType.NonFungible, assetId, 1, assertOwner: true); AddBalance(engine.SnapshotCache, assetId, account, 1); UInt160 uniqueId = GetNextNFTUniqueId(engine); - StorageKey key = CreateStorageKey(Prefix_NFTAccountToUniqueId, account, uniqueId); + StorageKey key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account, uniqueId); engine.SnapshotCache.Add(key, new()); key = CreateStorageKey(Prefix_NFTState, uniqueId); engine.SnapshotCache.Add(key, new(new NFTState @@ -239,7 +239,7 @@ internal async Task BurnNFT(ApplicationEngine engine, UInt160 uniqueId) if (!AddBalance(engine.SnapshotCache, nft.AssetId, nft.Owner, BigInteger.MinusOne)) throw new InvalidOperationException("Insufficient balance to burn."); engine.SnapshotCache.Delete(key); - key = CreateStorageKey(Prefix_NFTAccountToUniqueId, nft.Owner, uniqueId); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, nft.Owner, uniqueId); engine.SnapshotCache.Delete(key); await PostNFTTransferAsync(engine, uniqueId, nft.Owner, null, StackItem.Null, callOnPayment: false); } @@ -292,9 +292,9 @@ internal async Task TransferNFT(ApplicationEngine engine, UInt160 uniqueId if (!AddBalance(engine.SnapshotCache, nft.AssetId, from, BigInteger.MinusOne)) return false; AddBalance(engine.SnapshotCache, nft.AssetId, to, BigInteger.One); - key = CreateStorageKey(Prefix_NFTAccountToUniqueId, from, uniqueId); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, from, uniqueId); engine.SnapshotCache.Delete(key); - key = CreateStorageKey(Prefix_NFTAccountToUniqueId, to, uniqueId); + key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, to, uniqueId); engine.SnapshotCache.Add(key, new()); nft = engine.SnapshotCache.GetAndChange(key_nft)!.GetInteroperable(); nft.Owner = to; @@ -344,7 +344,7 @@ public IIterator GetNFTs(IReadOnlyStore snapshot) public IIterator GetNFTsOfOwner(IReadOnlyStore snapshot, UInt160 account) { const FindOptions options = FindOptions.KeysOnly | FindOptions.RemovePrefix; - var prefixKey = CreateStorageKey(Prefix_NFTAccountToUniqueId, account); + var prefixKey = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account); var enumerator = snapshot.Find(prefixKey).GetEnumerator(); return new StorageIterator(enumerator, 21, options); } From 2bd58e898547210abc712056a65b90f77ef0b221 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Fri, 5 Dec 2025 23:52:56 +0800 Subject: [PATCH 04/10] Add AssetId to UniqueId index --- src/Neo/SmartContract/Native/TokenManagement.cs | 16 ++++++++++++---- .../SmartContract/Native/UT_NativeContract.cs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Neo/SmartContract/Native/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs index df9c73ed71..02557004dd 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -31,6 +31,7 @@ public sealed class TokenManagement : NativeContract const byte Prefix_NFTUniqueIdSeed = 15; const byte Prefix_NFTState = 8; const byte Prefix_NFTOwnerUniqueIdIndex = 21; + const byte Prefix_NFTAssetIdUniqueIdIndex = 23; static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128); @@ -195,7 +196,9 @@ internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, AddTotalSupply(engine, TokenType.NonFungible, assetId, 1, assertOwner: true); AddBalance(engine.SnapshotCache, assetId, account, 1); UInt160 uniqueId = GetNextNFTUniqueId(engine); - StorageKey key = CreateStorageKey(Prefix_NFTOwnerUniqueIdIndex, account, uniqueId); + 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 @@ -239,6 +242,8 @@ internal async Task BurnNFT(ApplicationEngine engine, UInt160 uniqueId) 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); @@ -332,12 +337,15 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac } [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] - public IIterator GetNFTs(IReadOnlyStore snapshot) + 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_NFTState); + var prefixKey = CreateStorageKey(Prefix_NFTAssetIdUniqueIdIndex, assetId); var enumerator = snapshot.Find(prefixKey).GetEnumerator(); - return new StorageIterator(enumerator, 1, options); + return new StorageIterator(enumerator, 21, options); } [ContractMethod(CpuFee = 1 << 22, RequiredCallFlags = CallFlags.ReadStates)] diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 2f7f191d6b..c0373f4a7c 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":"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":"createNFT","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNFT","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":[],"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}}""" } + {"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":"createNFT","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNFT","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}}""" } }; } From 5911b9ed812d39afaeac75dd80150c171f1ab11e Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Sat, 6 Dec 2025 00:38:26 +0800 Subject: [PATCH 05/10] Add XML comments --- .../SmartContract/Native/TokenManagement.cs | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/src/Neo/SmartContract/Native/TokenManagement.cs b/src/Neo/SmartContract/Native/TokenManagement.cs index 02557004dd..f105f721a5 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -97,12 +97,29 @@ internal UInt160 Create(ApplicationEngine engine, [Length(1, 32)] string name, [ return tokenid; } + /// + /// 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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) { return CreateNFT(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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, BigInteger maxSupply) { @@ -160,12 +177,28 @@ internal async Task Mint(ApplicationEngine engine, UInt160 assetId, UInt160 acco await PostTransferAsync(engine, assetId, null, account, amount, StackItem.Null, callOnPayment: true); } + /// + /// 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) { @@ -232,6 +265,13 @@ internal async Task Burn(ApplicationEngine engine, UInt160 assetId, UInt160 acco await PostTransferAsync(engine, assetId, account, null, amount, StackItem.Null, callOnPayment: false); } + /// + /// 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) { @@ -282,6 +322,16 @@ internal async Task Transfer(ApplicationEngine engine, UInt160 assetId, UI return true; } + /// + /// 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) { @@ -329,6 +379,12 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac return accountState.Balance; } + /// + /// 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) { @@ -336,6 +392,21 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac 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) { @@ -348,6 +419,20 @@ public IIterator GetNFTs(IReadOnlyStore snapshot, UInt160 assetId) 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) { @@ -441,9 +526,18 @@ async ContractTask PostNFTTransferAsync(ApplicationEngine engine, UInt160 unique } } +/// +/// 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 } @@ -453,6 +547,9 @@ public enum TokenType : byte /// public class TokenState : IInteroperable { + /// + /// Specifies the type of token represented by this instance. + /// public required TokenType Type; /// @@ -513,10 +610,25 @@ public StackItem ToStackItem(IReferenceCounter? referenceCounter) 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; @@ -525,6 +637,11 @@ public void FromStackItem(StackItem stackItem) 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 }; From 712b2e29a7d356f0e60fcf96c429b6fc7b5880c4 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Sat, 6 Dec 2025 16:17:04 +0800 Subject: [PATCH 06/10] Split into multiple files --- src/Neo/SmartContract/Native/NFTState.cs | 60 ++ .../Native/TokenManagement.Fungible.cs | 153 +++++ .../Native/TokenManagement.NonFungible.cs | 289 ++++++++++ .../SmartContract/Native/TokenManagement.cs | 532 +----------------- src/Neo/SmartContract/Native/TokenState.cs | 84 +++ src/Neo/SmartContract/Native/TokenType.cs | 27 + 6 files changed, 619 insertions(+), 526 deletions(-) create mode 100644 src/Neo/SmartContract/Native/NFTState.cs create mode 100644 src/Neo/SmartContract/Native/TokenManagement.Fungible.cs create mode 100644 src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs create mode 100644 src/Neo/SmartContract/Native/TokenState.cs create mode 100644 src/Neo/SmartContract/Native/TokenType.cs 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..3424304870 --- /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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) + { + return CreateNFT(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 CreateNFT(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 = properties + })); + 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 f105f721a5..1f75815176 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.cs @@ -9,11 +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.SmartContract.Iterators; -using Neo.VM; -using Neo.VM.Types; using System.Numerics; namespace Neo.SmartContract.Native; @@ -22,128 +18,23 @@ namespace Neo.SmartContract.Native; /// Provides core functionality for creating, managing, and transferring tokens within a native contract environment. /// [ContractEvent(0, "Created", "assetId", ContractParameterType.Hash160, "type", ContractParameterType.Integer)] -[ContractEvent(1, "Transfer", "assetId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160, "amount", ContractParameterType.Integer)] -[ContractEvent(1, "NFTTransfer", "uniqueId", ContractParameterType.Hash160, "from", ContractParameterType.Hash160, "to", ContractParameterType.Hash160)] -public sealed class TokenManagement : NativeContract +public sealed partial class TokenManagement : NativeContract { const byte Prefix_TokenState = 10; const byte Prefix_AccountState = 12; - const byte Prefix_NFTUniqueIdSeed = 15; - const byte Prefix_NFTState = 8; - const byte Prefix_NFTOwnerUniqueIdIndex = 21; - const byte Prefix_NFTAssetIdUniqueIdIndex = 23; - - static readonly BigInteger MaxMintAmount = BigInteger.Pow(2, 128); internal TokenManagement() : base(-12) { } + partial void Initialize_Fungible(ApplicationEngine engine, Hardfork? hardfork); + partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork); + internal override ContractTask InitializeAsync(ApplicationEngine engine, Hardfork? hardfork) { - if (hardfork == ActiveIn) - { - engine.SnapshotCache.Add(CreateStorageKey(Prefix_NFTUniqueIdSeed), BigInteger.Zero); - } + Initialize_Fungible(engine, hardfork); + Initialize_NonFungible(engine, hardfork); return ContractTask.CompletedTask; } - /// - /// 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; - } - - /// - /// 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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) - { - return CreateNFT(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 CreateNFT(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; - } - /// /// Retrieves the token metadata for the given asset id. /// @@ -157,208 +48,6 @@ internal UInt160 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name return snapshot.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, TokenType.Fungible, assetId, amount, assertOwner: true); - AddBalance(engine.SnapshotCache, assetId, account, amount); - await PostTransferAsync(engine, assetId, null, account, amount, StackItem.Null, callOnPayment: true); - } - - /// - /// 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 = properties - })); - await PostNFTTransferAsync(engine, uniqueId, null, account, StackItem.Null, callOnPayment: true); - return uniqueId; - } - - /// - /// 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); - } - - /// - /// 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 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; - } - - /// - /// 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; - } - /// /// Returns the balance of for the specified . /// @@ -379,69 +68,6 @@ public BigInteger BalanceOf(IReadOnlyStore snapshot, UInt160 assetId, UInt160 ac return accountState.Balance; } - /// - /// 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); - } - /// /// Computes a unique asset id from the token owner's script hash and the token name. /// @@ -457,16 +83,6 @@ public static UInt160 GetAssetId(UInt160 owner, string name) return buffer.ToScriptHash(); } - 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(); - } - void AddTotalSupply(ApplicationEngine engine, TokenType type, UInt160 assetId, BigInteger amount, bool assertOwner) { StorageKey key = CreateStorageKey(Prefix_TokenState, assetId); @@ -510,140 +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); - } - - 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); - } -} - -/// -/// 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 -} - -/// -/// 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; - 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 }; - } -} - -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/TokenState.cs b/src/Neo/SmartContract/Native/TokenState.cs new file mode 100644 index 0000000000..3a5de158ee --- /dev/null +++ b/src/Neo/SmartContract/Native/TokenState.cs @@ -0,0 +1,84 @@ +// 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; + 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/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 +} From 71af80a61e8f2787f4b32e05331162ec236514fb Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Sat, 6 Dec 2025 16:24:43 +0800 Subject: [PATCH 07/10] Rename --- src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs | 6 +++--- .../Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs index 3424304870..3946c2059e 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs @@ -40,9 +40,9 @@ partial void Initialize_NonFungible(ApplicationEngine engine, Hardfork? hardfork /// 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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) + internal UInt160 CreateNonFungible(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol) { - return CreateNFT(engine, name, symbol, BigInteger.MinusOne); + return CreateNonFungible(engine, name, symbol, BigInteger.MinusOne); } /// @@ -56,7 +56,7 @@ internal UInt160 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name /// 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 CreateNFT(ApplicationEngine engine, [Length(1, 32)] string name, [Length(2, 6)] string symbol, BigInteger maxSupply) + 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!; diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index c0373f4a7c..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":"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":"createNFT","parameters":[{"name":"name","type":"String"},{"name":"symbol","type":"String"}],"returntype":"Hash160","offset":35,"safe":false},{"name":"createNFT","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}}""" } + {"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}}""" } }; } From bdca99eb885f8b4b69baa869c969f6b0fb09fddb Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Sat, 6 Dec 2025 16:30:28 +0800 Subject: [PATCH 08/10] Fix TokenState --- src/Neo/SmartContract/Native/TokenState.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Neo/SmartContract/Native/TokenState.cs b/src/Neo/SmartContract/Native/TokenState.cs index 3a5de158ee..de6d21946e 100644 --- a/src/Neo/SmartContract/Native/TokenState.cs +++ b/src/Neo/SmartContract/Native/TokenState.cs @@ -64,12 +64,13 @@ public class TokenState : IInteroperable 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(); + 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(); } /// @@ -79,6 +80,6 @@ public void FromStackItem(StackItem stackItem) /// A containing the token fields in order. public StackItem ToStackItem(IReferenceCounter? referenceCounter) { - return new Struct(referenceCounter) { Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply }; + return new Struct(referenceCounter) { (byte)Type, Owner.ToArray(), Name, Symbol, Decimals, TotalSupply, MaxSupply }; } } From 3119081cdfd96ceb9f4f2840b264591e2ba66468 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Thu, 11 Dec 2025 15:17:58 +0800 Subject: [PATCH 09/10] Revert changes to StorageItem.Set() --- src/Neo/SmartContract/StorageItem.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Neo/SmartContract/StorageItem.cs b/src/Neo/SmartContract/StorageItem.cs index c20ff4def3..5d88d4d32d 100644 --- a/src/Neo/SmartContract/StorageItem.cs +++ b/src/Neo/SmartContract/StorageItem.cs @@ -139,7 +139,9 @@ public void Seal() /// The integer to add. public BigInteger Add(BigInteger integer) { - return Set(this + integer); + BigInteger result = this + integer; + Set(result); + return result; } /// @@ -277,11 +279,10 @@ public void Serialize(BinaryWriter writer) /// Sets the integer value of the storage. /// /// The integer value to set. - public BigInteger Set(BigInteger integer) + public void Set(BigInteger integer) { _cache = integer; _value = null; - return integer; } public static implicit operator BigInteger(StorageItem item) From e665a284525e2c7b18ce62943245458b7c8ac6f8 Mon Sep 17 00:00:00 2001 From: Erik Zhang Date: Thu, 11 Dec 2025 15:52:44 +0800 Subject: [PATCH 10/10] DeepCopy --- src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs index 3946c2059e..e08e346861 100644 --- a/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs +++ b/src/Neo/SmartContract/Native/TokenManagement.NonFungible.cs @@ -140,7 +140,7 @@ internal async Task MintNFT(ApplicationEngine engine, UInt160 assetId, { AssetId = assetId, Owner = account, - Properties = properties + Properties = (Map)properties.DeepCopy(asImmutable: true) })); await PostNFTTransferAsync(engine, uniqueId, null, account, StackItem.Null, callOnPayment: true); return uniqueId;