Skip to content

Commit

Permalink
Neo Shell extension model (#4)
Browse files Browse the repository at this point in the history
* added worknet command dynamically

* can pass input parameter

* removed unused console print

* created shell extension abstractions and execute method for the shell extension

* can map subcommand to the real extenion command for example map worknet to neo-worknet

* -added initial nft transfer command to test out shell extension that can handle transactions

Still WIP

* fix warnings, still experimenting the code is not up to prod standard

* nft transfer seems to be work with 0X hash format, also added owner of

* can invoke for result or submit

* transfer and onwer of working with passing script directly

* refactored the code

* code clean up

* added NEO extension readme

* typo

* Addressed review comments

* fixed logo reference that broke 'dotnet pack'

* remove logo from neonft since it's a sample project

---------

Co-authored-by: Weijie Lin <[email protected]>
  • Loading branch information
ngdentdev and cloudmation authored May 2, 2023
1 parent 6a961c1 commit 92fed18
Show file tree
Hide file tree
Showing 14 changed files with 683 additions and 163 deletions.
92 changes: 89 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,30 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neo-shell.dll",
"args": ["contract", "run", "DevHawk.Registrar", "query", "sample.domain", "--results"],
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["contract", "run", "NeoContributorToken", "transfer", "0xf898fec9055cc080f46ed38f2a7430b9b245a5a8", "0x1727A97FFDE4DC46E687959ED82D038BF5AEF5FF384AAFBA246D23629B3FC675","data","--account node1"],
"cwd": "${workspaceFolder}/src",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "nft transfer",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["nft", "transfer", "NeoContributorToken", "0xf898fec9055cc080f46ed38f2a7430b9b245a5a8", "0x1727A97FFDE4DC46E687959ED82D038BF5AEF5FF384AAFBA246D23629B3FC675","anything","-a", "node1"],
"cwd": "${workspaceFolder}/src",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "nft owner of",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["nft", "owner", "NeoContributorToken", "0xDA21F035A927F25105F02F71CD6D5266F19742B703CC5FEE6369E036D3E3A168", "-a", "node1"],
"cwd": "${workspaceFolder}/src",
"console": "internalConsole",
"stopAtEntry": false
Expand All @@ -78,12 +100,76 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neo-shell.dll",
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["show", "balance", "0x779f3fad83057b2dd0d74edbeb29e83f8c8ae5b5", "node1"],
"cwd": "${workspaceFolder}/src",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": "wallet list(ext)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["neo-worknet","wallet", "list", "--json"],
"cwd": "${workspaceFolder}/src",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": "neosh nft transfer",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["nft","transfer", "0x1fc6ede613b67a4bc20cefb103738c948bd0b1fe","0xf898fec9055cc080f46ed38f2a7430b9b245a5a8", "0xFD3C2163A6B3816AD243BF4E6DE6A363B285B63D4757108796B27D1DC10C6F7A","node1"],
"cwd": "${workspaceFolder}/src",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": "neosh nft ownerOf",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/shell/bin/Debug/net6.0/neosh",
"args": ["nft","ownerOf", "0x1fc6ede613b67a4bc20cefb103738c948bd0b1fe", "0xFD3C2163A6B3816AD243BF4E6DE6A363B285B63D4757108796B27D1DC10C6F7A"],
"cwd": "${workspaceFolder}/src",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": "nft transfer",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/nft/bin/Debug/net6.0/neonft",
"args": ["transfer", "0x1fc6ede613b67a4bc20cefb103738c948bd0b1fe","0xf898fec9055cc080f46ed38f2a7430b9b245a5a8","0xFD3C2163A6B3816AD243BF4E6DE6A363B285B63D4757108796B27D1DC10C6F7A","node1"],
"cwd": "${workspaceFolder}/src",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
}
]
}
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,66 @@ package feed in a
Several Neo sample projects like
[NeoContributorToken](https://github.com/ngdenterprise/neo-contrib-token)
use a NuGet.config file.

## Extending NEO Shell

NEO Shell allows developers to extend its functionality by adding custom commands. To do this, create a ~/.neo/extensions.json file that contains a list of commands executable from the shell. NEO Shell communicates with extensions using standard input/output.

There are two types of extensions.

1. NEO shell handles connections to the network. All commands are available through NEO shell. The following is an example of a `~/.neo/extensions.json` file that adds all `worknet` commands to the shell. The only requirement is that the command needs to implement an --Input parameter. This parameter is used to pass the network connection information to the command. "mapsToCommand" value can be a full path to the executable.

```json
[
{
"name": "NEO Worknet",
"command": "worknet",
"mapsToCommand": "neo-worknet"
}
]
```

```json
[
{
"name": "NEO Worknet",
"command": "worknet",
"mapsToCommand": "neo-worknet"
}
]
```

An example command looks like this: `neosh neo-worknet storage get 0x5423fc51fea5ac443759323bbbccdc922cd3311c 0x17F9075AE0136F96FA4EE537CE667989A88DE65A1C31373031`

2. In addition to handling connections to the network, NEO shell can also invoke smart contracts on behalf of the commands. This is done by adding the `invokeContract` and `safe` parameters to the extension. The `invokeContract` parameter is used to indicate that the command will invoke a smart contract. The `safe` parameter is used to indicate that the command will not change the state of the blockchain. The following is an example of a `~/.neo/extensions.json` file that adds all `nft` commands to the shell. The `nft` command has two commands that can be invoked. The `transfer` command will change the state of the blockchain. The `ownerOf` command will not change the state of the blockchain.

```json
[
{
"name": "NEO NFT",
"command": "nft",
"mapsToCommand": "neonft",
"commands": [
{
"command": "transfer",
"invokeContract": true,
"safe": false
},
{
"command": "ownerOf",
"invokeContract": true,
"safe": true
}
]
}
]
```

The extension commands are required to pass unsigned scripts to the NEO shell through standard out. The NEO shell will sign the scripts, execute the contract and output the result through standard out. The following is an example of a `neonft` command that will transfer an NFT from one address to another. The following snippet from the [NeoNFT] project shows how to pass the unsigned script to the NEO shell.

```csharp
...
var script = contractHash.MakeScript("transfer", toHash, idBytes, string.Empty);
var payload = new { Script = Convert.ToBase64String(script), Account = this.Account, Trace = this.Trace, Json = this.Json };
Console.WriteLine(JsonConvert.SerializeObject(payload));
```
1 change: 1 addition & 0 deletions dirs.proj
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<ItemGroup>
<ProjectReference Include="src\worknet\neoworknet.csproj" />
<ProjectReference Include="src\shell\neoshell.csproj" />
<ProjectReference Include="src\shell-ext\nft\neonft.csproj" />
</ItemGroup>
</Project>
4 changes: 0 additions & 4 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@
<NeoMonorepoPath>..\..\..\..\official\3neo-monorepo</NeoMonorepoPath>
</PropertyGroup>

<ItemGroup>
<None Include="../neo-logo-72.png" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

</Project>
55 changes: 55 additions & 0 deletions src/shell-ext/nft/Commands/OwnerOfCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Abstractions;
using McMaster.Extensions.CommandLineUtils;
using Neo;
using Neo.VM;
using Neo.VM.Types;
using Newtonsoft.Json;

namespace NeoNft.Commands
{
[Command("ownerof", Description = "Transfer a NFT to another address")]
partial class OwnerOfCommand
{
[Argument(0, Description = "Contract hash of the NFT contract")]
[Required]
internal string Contract { get; init; } = string.Empty;

[Argument(1, Description = "NFT ID")]
[Required]
internal string Id { get; init; } = string.Empty;

[Option(Description = "Path to neo data file")]
internal string Input { get; init; } = string.Empty;

[Option(Description = "Enable contract execution tracing")]
internal bool Trace { get; init; } = false;

[Option(Description = "Output as JSON")]
internal bool Json { get; init; } = false;

internal int OnExecute(CommandLineApplication app, IConsole console)
{
try
{
UInt160.TryParse(this.Contract, out var contractHash);
var hexString = this.Id;
if (this.Id.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
hexString = hexString.Substring(2);
}

var idBytes = hexString.HexToBytes();
var script = contractHash.MakeScript("ownerOf", idBytes);
var payload = new { Script = Convert.ToBase64String(script), Trace = this.Trace, Json = this.Json };
Console.WriteLine(JsonConvert.SerializeObject(payload));
return 0;
}
catch (Exception ex)
{
app.Error.Write(ex.Message);
return 1;
}
}
}
}
66 changes: 66 additions & 0 deletions src/shell-ext/nft/Commands/TransferCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.ComponentModel.DataAnnotations;
using System.IO.Abstractions;
using McMaster.Extensions.CommandLineUtils;
using Neo;
using Neo.BlockchainToolkit.Models;
using Neo.VM;
using Newtonsoft.Json;

namespace NeoNft.Commands
{
[Command("transfer", Description = "Transfer a NFT to another address")]
partial class TransferCommand
{
[Option(Description = "Path to neo data file")]
internal string Input { get; init; } = string.Empty;

[Argument(0, Description = "Contract hash of the NFT contract")]
[Required]
internal string Contract { get; init; } = string.Empty;

[Argument(1, Description = "Address to transfer to")]
[Required]
internal string To { get; init; } = string.Empty;

[Argument(2, Description = "NFT ID to transfer")]
[Required]
internal string Id { get; init; } = string.Empty;

[Argument(3, Description = "NFT contract owner account")]
[Required]
internal string Account { get; init; } = string.Empty;

[Option(Description = "Enable contract execution tracing")]
internal bool Trace { get; init; } = false;

[Option(Description = "Output as JSON")]
internal bool Json { get; init; } = false;



internal int OnExecute(CommandLineApplication app, IConsole console)
{
try
{
UInt160.TryParse(this.Contract, out var contractHash);
UInt160.TryParse(this.To, out var toHash);
var hexString = this.Id;
if (this.Id.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
hexString = hexString.Substring(2);
}

var idBytes = hexString.HexToBytes();
var script = contractHash.MakeScript("transfer", toHash, idBytes, string.Empty);
var payload = new { Script = Convert.ToBase64String(script), Account = this.Account, Trace = this.Trace, Json = this.Json };
Console.WriteLine(JsonConvert.SerializeObject(payload));
return 0;
}
catch (Exception ex)
{
app.Error.Write(ex.Message);
return 1;
}
}
}
}
25 changes: 25 additions & 0 deletions src/shell-ext/nft/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.IO.Abstractions;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using NeoNft.Commands;

namespace NeoNft
{
[Command("nft")]
[Subcommand(typeof(TransferCommand), typeof(OwnerOfCommand))]
class Program
{
public static Task<int> Main(string[] args)
{
var services = new ServiceCollection()
.AddSingleton<IConsole>(PhysicalConsole.Singleton)
.BuildServiceProvider();

var app = new CommandLineApplication<Program>();
app.Conventions
.UseDefaultConventions()
.UseConstructorInjection(services);
return app.ExecuteAsync(args);
}
}
}
34 changes: 34 additions & 0 deletions src/shell-ext/nft/neonft.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>neonft</AssemblyName>
<OutputType>Exe</OutputType>
<PackageId>Neo.Nft</PackageId>
<PackAsTool>true</PackAsTool>
<RootNamespace>NeoNft</RootNamespace>
<PackageIcon />
</PropertyGroup>

<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<None Include="../../neo-logo-72.png" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>

<Choose>
<When Condition=" '$(BlockchainToolkitLibraryVersion)' == 'local'">
<ItemGroup>
<ProjectReference Include="$(BlockchainToolkitLibraryLocalPath)\src\bctklib\bctklib.csproj" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<PackageReference Include="Neo.BlockchainToolkit.Library" Version="$(BlockchainToolkitLibraryVersion)" />
</ItemGroup>
</Otherwise>
</Choose>

</Project>
Loading

0 comments on commit 92fed18

Please sign in to comment.