diff --git a/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs b/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs index abb1aacd..de41880d 100644 --- a/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs +++ b/src/neoxp/Commands/BatchCommand.BatchFileCommands.cs @@ -19,7 +19,7 @@ namespace NeoExpress.Commands partial class BatchCommand { [Command] - [Subcommand(typeof(Checkpoint), typeof(Contract), typeof(FastForward), typeof(Oracle), typeof(Policy), typeof(Transfer), typeof(Wallet))] + [Subcommand(typeof(Checkpoint), typeof(Contract), typeof(FastForward), typeof(Oracle), typeof(Policy), typeof(Transfer), typeof(TransferNFT), typeof(Wallet))] internal class BatchFileCommands { [Command("checkpoint")] @@ -292,6 +292,31 @@ internal class Transfer [Option(Description = "password to use for NEP-2/NEP-6 sender")] internal string Password { get; init; } = string.Empty; } + [Command("transfernft")] + internal class TransferNFT + { + [Argument(0, Description = "NFT Contract (Symbol or Script Hash)")] + [Required] + internal string Contract { get; init; } = string.Empty; + + [Argument(1, Description = "TokenId of NFT (Format: HEX, BASE64)")] + [Required] + internal string TokenId { get; init; } = string.Empty; + + [Argument(2, Description = "Account to send NFT from (Format: Wallet name, WIF)")] + [Required] + internal string Sender { get; init; } = string.Empty; + + [Argument(3, Description = "Account to send NFT to (Format: Script Hash, Address, Wallet name)")] + [Required] + internal string Receiver { get; init; } = string.Empty; + + [Option(Description = "Optional data parameter to pass to transfer operation")] + internal string Data { get; init; } = string.Empty; + + [Option(Description = "password to use for NEP-2/NEP-6 sender")] + internal string Password { get; init; } = string.Empty; + } [Command("wallet")] [Subcommand(typeof(Create))] diff --git a/src/neoxp/Commands/BatchCommand.cs b/src/neoxp/Commands/BatchCommand.cs index 93b111b5..64163118 100644 --- a/src/neoxp/Commands/BatchCommand.cs +++ b/src/neoxp/Commands/BatchCommand.cs @@ -267,6 +267,17 @@ await txExec.TransferAsync( cmd.Model.Data).ConfigureAwait(false); break; } + case CommandLineApplication cmd: + { + await txExec.TransferNFTAsync( + cmd.Model.Contract, + cmd.Model.TokenId, + cmd.Model.Sender, + cmd.Model.Password, + cmd.Model.Receiver, + cmd.Model.Data).ConfigureAwait(false); + break; + } case CommandLineApplication cmd: { var wallet = chainManager.CreateWallet( diff --git a/src/neoxp/Commands/ShowCommand.NFT.cs b/src/neoxp/Commands/ShowCommand.NFT.cs index 639cd2e2..7e3861dc 100644 --- a/src/neoxp/Commands/ShowCommand.NFT.cs +++ b/src/neoxp/Commands/ShowCommand.NFT.cs @@ -12,7 +12,6 @@ using McMaster.Extensions.CommandLineUtils; using Neo; using System.ComponentModel.DataAnnotations; -using System.Text; namespace NeoExpress.Commands { @@ -28,11 +27,11 @@ public NFT(ExpressChainManagerFactory chainManagerFactory) this.chainManagerFactory = chainManagerFactory; } - [Argument(0, Description = "Contract to show NFT of (symbol or script hash)")] + [Argument(0, Description = "NFT Contract (Symbol or Script Hash)")] [Required] internal string Contract { get; init; } = string.Empty; - [Argument(1, Description = "Account to show asset balance for")] + [Argument(1, Description = "Account to show NFT (Format: Script Hash, Address, Wallet name)")] [Required] internal string Account { get; init; } = string.Empty; @@ -45,20 +44,33 @@ internal async Task OnExecuteAsync(CommandLineApplication app, IConsole con { var (chainManager, _) = chainManagerFactory.LoadChain(Input); using var expressNode = chainManager.GetExpressNode(); - - var getHashResult = await expressNode.TryGetAccountHashAsync(chainManager.Chain, Account).ConfigureAwait(false); - if (getHashResult.TryPickT1(out _, out var accountHash)) + if (!UInt160.TryParse(Account, out var accountHash)) //script hash { - throw new Exception($"{Account} account not found."); + if (!chainManager.Chain.TryParseScriptHash(Account, out accountHash)) //address + { + var getHashResult = await expressNode.TryGetAccountHashAsync(chainManager.Chain, Account).ConfigureAwait(false); //wallet name + if (getHashResult.TryPickT1(out _, out accountHash)) + { + throw new Exception($"{Account} account not found."); + } + } } - - var list = await expressNode.GetNFTAsync(accountHash, Contract).ConfigureAwait(false); - list.ForEach(p => console.Out.WriteLine($"TokenId: {p}, TokenId(Hex): {Encoding.UTF8.GetBytes(p).ToHexString()}")); + var parser = await expressNode.GetContractParameterParserAsync(chainManager.Chain).ConfigureAwait(false); + var scriptHash = parser.TryLoadScriptHash(Contract, out var value) + ? value + : UInt160.TryParse(Contract, out var uint160) + ? uint160 + : throw new InvalidOperationException($"contract \"{Contract}\" not found"); + var list = await expressNode.GetNFTAsync(accountHash, scriptHash).ConfigureAwait(false); + if (list.Count == 0) + await console.Out.WriteLineAsync($"No NFT yet. (Contract:{scriptHash}, Account:{accountHash})"); + else + list.ForEach(p => console.Out.WriteLine($"TokenId(Base64): {p}, TokenId(Hex): 0x{Convert.FromBase64String(p).Reverse().ToArray().ToHexString()}")); return 0; } catch (Exception ex) { - app.WriteException(ex); + app.WriteException(ex, true); return 1; } } diff --git a/src/neoxp/Commands/TransferNFTCommand.cs b/src/neoxp/Commands/TransferNFTCommand.cs index 48c7ae20..ed82c47d 100644 --- a/src/neoxp/Commands/TransferNFTCommand.cs +++ b/src/neoxp/Commands/TransferNFTCommand.cs @@ -29,19 +29,19 @@ public TransferNFTCommand(ExpressChainManagerFactory chainManagerFactory, Transa this.txExecutorFactory = txExecutorFactory; } - [Argument(0, Description = "NFT Contract (symbol or script hash)")] + [Argument(0, Description = "NFT Contract (Symbol or Script Hash)")] [Required] internal string Contract { get; init; } = string.Empty; - [Argument(1, Description = "TokenId of NFT (Hex string)")] + [Argument(1, Description = "TokenId of NFT (Format: HEX, BASE64)")] [Required] internal string TokenId { get; init; } = string.Empty; - [Argument(2, Description = "Account to send NFT from")] + [Argument(2, Description = "Account to send NFT from (Format: Wallet name, WIF)")] [Required] internal string Sender { get; init; } = string.Empty; - [Argument(3, Description = "Account to send NFT to")] + [Argument(3, Description = "Account to send NFT to (Format: Script Hash, Address, Wallet name)")] [Required] internal string Receiver { get; init; } = string.Empty; @@ -67,7 +67,7 @@ internal async Task OnExecuteAsync(CommandLineApplication app, IConsole con var (chainManager, _) = chainManagerFactory.LoadChain(Input); var password = chainManager.Chain.ResolvePassword(Sender, Password); using var txExec = txExecutorFactory.Create(chainManager, Trace, Json); - await txExec.TransferNFTAsync(Contract, HexStringToUTF8(TokenId), Sender, password, Receiver, Data).ConfigureAwait(false); + await txExec.TransferNFTAsync(Contract, HexOrBase64ToUTF8(TokenId), Sender, password, Receiver, Data).ConfigureAwait(false); return 0; } catch (Exception ex) @@ -76,19 +76,15 @@ internal async Task OnExecuteAsync(CommandLineApplication app, IConsole con return 1; } - static string HexStringToUTF8(string hex) + static string HexOrBase64ToUTF8(string input) { - hex = hex.ToLower().Trim(); - if (!new Regex("^(0x)?([0-9a-f]{2})+$").IsMatch(hex)) - throw new FormatException(); - - if (new Regex("^([0-9a-f]{2})+$").IsMatch(hex)) + try { - return Encoding.UTF8.GetString(hex.HexToBytes()); + return input.StartsWith("0x") ? Encoding.UTF8.GetString(input[2..].HexToBytes().Reverse().ToArray()) : Encoding.UTF8.GetString(Convert.FromBase64String(input)); } - else + catch (Exception) { - return Encoding.UTF8.GetString(hex[2..].HexToBytes().Reverse().ToArray()); + throw new ArgumentException($"Unknown Asset \"{input}\"", nameof(TokenId)); } } } diff --git a/src/neoxp/Extensions/ExpressNodeExtensions.cs b/src/neoxp/Extensions/ExpressNodeExtensions.cs index 6a9cfc04..d58abf96 100644 --- a/src/neoxp/Extensions/ExpressNodeExtensions.cs +++ b/src/neoxp/Extensions/ExpressNodeExtensions.cs @@ -445,10 +445,8 @@ byte[] GetResponseData(string filter) throw new Exception("invalid script results"); } - public static async Task> GetNFTAsync(this IExpressNode expressNode, UInt160 accountHash, string asset) + public static async Task> GetNFTAsync(this IExpressNode expressNode, UInt160 accountHash, UInt160 assetHash) { - var assetHash = await expressNode.ParseAssetAsync(asset).ConfigureAwait(false); - using var sb = new ScriptBuilder(); sb.EmitDynamicCall(assetHash, "tokensOf", accountHash); @@ -464,7 +462,7 @@ public static async Task> GetNFTAsync(this IExpressNode expressNode { while (iterator.Next()) { - list.Add(iterator.Value(null).GetString()!); + list.Add(Convert.ToBase64String(iterator.Value(null).GetSpan())); } } } diff --git a/src/neoxp/TransactionExecutor.cs b/src/neoxp/TransactionExecutor.cs index 776a1027..e54fe66f 100644 --- a/src/neoxp/TransactionExecutor.cs +++ b/src/neoxp/TransactionExecutor.cs @@ -452,10 +452,16 @@ public async Task TransferNFTAsync(string contract, string tokenId, string sende throw new Exception($"{sender} sender not found."); } - var getHashResult = await expressNode.TryGetAccountHashAsync(chainManager.Chain, receiver).ConfigureAwait(false); - if (getHashResult.TryPickT1(out _, out var receiverHash)) + if (!UInt160.TryParse(receiver, out var receiverHash)) //script hash { - throw new Exception($"{receiver} account not found."); + if (!chainManager.Chain.TryParseScriptHash(receiver, out receiverHash)) //address + { + var getHashResult = await expressNode.TryGetAccountHashAsync(chainManager.Chain, receiver).ConfigureAwait(false); //wallet name + if (getHashResult.TryPickT1(out _, out receiverHash)) + { + throw new Exception($"{receiver} account not found."); + } + } } ContractParameter? dataParam = null; @@ -464,8 +470,12 @@ public async Task TransferNFTAsync(string contract, string tokenId, string sende var parser = await expressNode.GetContractParameterParserAsync(chainManager.Chain).ConfigureAwait(false); dataParam = parser.ParseParameter(data); } - - var assetHash = await expressNode.ParseAssetAsync(contract).ConfigureAwait(false); + var parser2 = await expressNode.GetContractParameterParserAsync(chainManager.Chain).ConfigureAwait(false); + var assetHash = parser2.TryLoadScriptHash(contract, out var value) + ? value + : UInt160.TryParse(contract, out var uint160) + ? uint160 + : throw new InvalidOperationException($"contract \"{contract}\" not found"); var txHash = await expressNode.TransferNFTAsync(assetHash, tokenId, senderWallet, senderAccountHash, receiverHash, dataParam); await writer.WriteTxHashAsync(txHash, "TransferNFT", json).ConfigureAwait(false); }