Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Neo/SmartContract/InteropParameterDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
60 changes: 60 additions & 0 deletions src/Neo/SmartContract/Native/NFTState.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents the state of a non-fungible token (NFT), including its asset identifier, owner, and associated properties.
/// Implements <see cref="IInteroperable"/> to allow conversion to/from VM <see cref="StackItem"/>.
/// </summary>
public class NFTState : IInteroperable
{
/// <summary>
/// The asset id (collection) this NFT belongs to.
/// </summary>
public required UInt160 AssetId;

/// <summary>
/// The account (owner) that currently owns this NFT.
/// </summary>
public required UInt160 Owner;

/// <summary>
/// Arbitrary properties associated with this NFT. Keys are ByteString and values are ByteString or Buffer.
/// </summary>
public required Map Properties;

/// <summary>
/// Populates this instance from a VM <see cref="StackItem"/> representation.
/// </summary>
/// <param name="stackItem">A <see cref="StackItem"/> expected to be a <see cref="Struct"/> with fields in the order: AssetId, Owner, Properties.</param>
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];
}

/// <summary>
/// Convert current NFTState to a VM <see cref="StackItem"/> (Struct).
/// </summary>
/// <param name="referenceCounter">Optional reference counter used by the VM.</param>
/// <returns>A <see cref="Struct"/> representing the NFTState.</returns>
public StackItem ToStackItem(IReferenceCounter? referenceCounter)
{
return new Struct(referenceCounter) { AssetId.ToArray(), Owner.ToArray(), Properties };
}
}
153 changes: 153 additions & 0 deletions src/Neo/SmartContract/Native/TokenManagement.Fungible.cs
Original file line number Diff line number Diff line change
@@ -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);

/// <summary>
/// Creates a new token with an unlimited maximum supply.
/// </summary>
/// <param name="engine">The current <see cref="ApplicationEngine"/> instance.</param>
/// <param name="name">The token name (1-32 characters).</param>
/// <param name="symbol">The token symbol (2-6 characters).</param>
/// <param name="decimals">The number of decimals (0-18).</param>
/// <returns>The asset <see cref="UInt160"/> identifier generated for the new token.</returns>
/// <exception cref="ArgumentOutOfRangeException">If parameter constraints are violated.</exception>
/// <exception cref="InvalidOperationException">If a token with the same id already exists.</exception>
[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);
}

/// <summary>
/// Creates a new token with a specified maximum supply.
/// </summary>
/// <param name="engine">The current <see cref="ApplicationEngine"/> instance.</param>
/// <param name="name">The token name (1-32 characters).</param>
/// <param name="symbol">The token symbol (2-6 characters).</param>
/// <param name="decimals">The number of decimals (0-18).</param>
/// <param name="maxSupply">Maximum total supply, or -1 for unlimited.</param>
/// <returns>The asset <see cref="UInt160"/> identifier generated for the new token.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="maxSupply"/> is less than -1.</exception>
/// <exception cref="InvalidOperationException">If a token with the same id already exists.</exception>
[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;
}

/// <summary>
/// Mints new tokens to an account. Only the token owner contract may call this method.
/// </summary>
/// <param name="engine">The current <see cref="ApplicationEngine"/> instance.</param>
/// <param name="assetId">The asset identifier.</param>
/// <param name="account">The recipient account <see cref="UInt160"/>.</param>
/// <param name="amount">The amount to mint (must be > 0 and &lt;= <see cref="MaxMintAmount"/>).</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="amount"/> is invalid.</exception>
/// <exception cref="InvalidOperationException">If the asset id does not exist or caller is not the owner or max supply would be exceeded.</exception>
[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);
}

/// <summary>
/// Burns tokens from an account, decreasing the total supply. Only the token owner contract may call this method.
/// </summary>
/// <param name="engine">The current <see cref="ApplicationEngine"/> instance.</param>
/// <param name="assetId">The asset identifier.</param>
/// <param name="account">The account <see cref="UInt160"/> from which tokens will be burned.</param>
/// <param name="amount">The amount to burn (must be > 0 and &lt;= <see cref="MaxMintAmount"/>).</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="amount"/> is invalid.</exception>
/// <exception cref="InvalidOperationException">If the asset id does not exist, caller is not the owner, or account has insufficient balance.</exception>
[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);
}

/// <summary>
/// Transfers tokens between accounts.
/// </summary>
/// <param name="engine">The current <see cref="ApplicationEngine"/> instance.</param>
/// <param name="assetId">The asset identifier.</param>
/// <param name="from">The sender account <see cref="UInt160"/>.</param>
/// <param name="to">The recipient account <see cref="UInt160"/>.</param>
/// <param name="amount">The amount to transfer (must be &gt;= 0).</param>
/// <param name="data">Arbitrary data passed to <c>onPayment</c> or <c>onTransfer</c> callbacks.</param>
/// <returns><c>true</c> if the transfer succeeded; otherwise <c>false</c>.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="amount"/> is negative.</exception>
/// <exception cref="InvalidOperationException">If the asset id does not exist.</exception>
[ContractMethod(CpuFee = 1 << 17, StorageFee = 1 << 7, RequiredCallFlags = CallFlags.All)]
internal async Task<bool> 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<TokenState>()
?? 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);
}
}
Loading