├── src ├── rchia │ ├── chia.ico │ ├── IStatus.cs │ ├── Commands │ │ ├── CommandTargetAttribute.cs │ │ ├── CommandAttribute.cs │ │ ├── ArgumentAttribute.cs │ │ ├── ConsoleStatus.cs │ │ ├── OptionAttribute.cs │ │ ├── JsonOutput.cs │ │ ├── Command.cs │ │ ├── AttributeExtensions.cs │ │ └── ConsoleOutput.cs │ ├── Endpoint.cs │ ├── Properties │ │ ├── launchSettings.json │ │ └── GlobalSuppressions.cs │ ├── Farm │ │ ├── FarmCommands.cs │ │ ├── ChallengesCommand.cs │ │ └── SummaryCommand.cs │ ├── Bech32 │ │ ├── Bech32Commands.cs │ │ ├── HashFromAddressCommand.cs │ │ └── AddressFromHashCommand.cs │ ├── Program.cs │ ├── Services │ │ ├── ServicesCommands.cs │ │ ├── ServiceGroups.cs │ │ ├── ListServicesCommand.cs │ │ ├── StartServicesCommand.cs │ │ └── StopServicesCommand.cs │ ├── Blocks │ │ ├── BlocksCommands.cs │ │ ├── RecentBlocksCommand.cs │ │ ├── AdditionsAndRemovalsCommand.cs │ │ └── BlockHeaderCommand.cs │ ├── StatusMessage.cs │ ├── Endpoints │ │ ├── ShowEndpointCommand.cs │ │ ├── RemoveEndpointCommand.cs │ │ ├── ListEndpointsCommand.cs │ │ ├── SetDefaultEndpointCommand.cs │ │ ├── EndpointsCommands.cs │ │ ├── TestEndpointCommand.cs │ │ └── AddEndpointCommand.cs │ ├── Node │ │ ├── VersionCommand.cs │ │ ├── StopNodeCommand.cs │ │ ├── NodeCommands.cs │ │ ├── PingCommand.cs │ │ ├── NetspaceCommand.cs │ │ └── StatusCommand.cs │ ├── Plots │ │ ├── RefreshPlotsCommand.cs │ │ ├── AddPlotsCommand.cs │ │ ├── RemovePlotsCommand.cs │ │ ├── ShowDirectoriesCommand.cs │ │ ├── ListPlottersCommand.cs │ │ ├── PlotsCommands.cs │ │ ├── ShowPlotQueueCommand.cs │ │ ├── ShowPlotLogCommand.cs │ │ ├── ListPlotsCommand.cs │ │ └── CreatePlotsCommand.cs │ ├── Connections │ │ ├── ConnectionsCommands.cs │ │ ├── RemoveConnectionCommand.cs │ │ ├── AddConnectionCommand.cs │ │ ├── ListConnectionsCommand.cs │ │ └── PruneConnectionsCommand.cs │ ├── ICommandOutput.cs │ ├── Keys │ │ ├── GenerateKeyCommand.cs │ │ ├── GenerateAndPrintKeyCommand.cs │ │ ├── KeysCommands.cs │ │ ├── DeleteAllKeysCommand.cs │ │ ├── DeleteKeyCommand.cs │ │ ├── AddKeyCommand.cs │ │ └── ShowKeysCommand.cs │ ├── Wallets │ │ ├── GetAddressCommand.cs │ │ ├── DeleteUnconfirmedTransactionsCommand.cs │ │ ├── WalletsCommands.cs │ │ ├── GetTransactionCommand.cs │ │ ├── ListTransactionsCommand.cs │ │ ├── SendTransactionCommand.cs │ │ └── ShowWalletCommand.cs │ ├── PlotNft │ │ ├── PlotNftCommands.cs │ │ ├── InspectNftCommand.cs │ │ ├── GetLoginLinkCommand.cs │ │ ├── LeavePoolCommand.cs │ │ ├── ClaimNftCommand.cs │ │ ├── JoinPoolCommand.cs │ │ ├── CreatePlotNftCommand.cs │ │ └── ShowPlotNftCommand.cs │ ├── EndpointOptions.cs │ ├── Formattable.cs │ ├── EndpointLibrary.cs │ ├── Settings.cs │ ├── rchia.csproj │ ├── WalletCommand.cs │ ├── Extensions.cs │ └── ClientFactory.cs ├── rchia.sln ├── test.ps1 ├── publish-all.ps1 └── .editorconfig ├── chia-leaf-logo-384x384.png ├── .github ├── dependabot.yml ├── workflows │ ├── dotnet.yml │ └── codeql-analysis.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .vscode ├── tasks.json └── launch.json ├── README.md ├── .gitignore └── LICENSE /src/rchia/chia.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkackman/rchia/HEAD/src/rchia/chia.ico -------------------------------------------------------------------------------- /chia-leaf-logo-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkackman/rchia/HEAD/chia-leaf-logo-384x384.png -------------------------------------------------------------------------------- /src/rchia/IStatus.cs: -------------------------------------------------------------------------------- 1 | namespace rchia; 2 | 3 | public interface IStatus 4 | { 5 | string Status { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/rchia/Commands/CommandTargetAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace rchia.Commands; 4 | 5 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 6 | public sealed class CommandTargetAttribute : Attribute 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/rchia/Endpoint.cs: -------------------------------------------------------------------------------- 1 | using chia.dotnet; 2 | 3 | namespace rchia; 4 | 5 | public record Endpoint 6 | { 7 | public string Id { get; init; } = string.Empty; 8 | 9 | public bool IsDefault { get; set; } 10 | 11 | public EndpointInfo EndpointInfo { get; init; } = new(); 12 | } 13 | -------------------------------------------------------------------------------- /src/rchia/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "rchia": { 4 | "commandName": "Project", 5 | "commandLineArgs": "node status -ep former" 6 | }, 7 | "WSL": { 8 | "commandName": "WSL2", 9 | "environmentVariables": {}, 10 | "distributionName": "" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/rchia/Commands/CommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace rchia.Commands; 4 | 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 6 | public class CommandAttribute : Attribute 7 | { 8 | public CommandAttribute(string name) 9 | { 10 | Name = name; 11 | } 12 | 13 | public string Name { get; init; } 14 | 15 | public string? Description { get; init; } 16 | } 17 | -------------------------------------------------------------------------------- /src/rchia/Properties/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "", Scope = "namespace", Target = "~N:rchia")] 9 | -------------------------------------------------------------------------------- /src/rchia/Farm/FarmCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Farm; 4 | 5 | [Command("farm", Description = "Manage your farm.\nRequires a daemon endpoint.")] 6 | internal sealed class FarmCommands 7 | { 8 | [Command("challenges", Description = "Show the latest challenges")] 9 | public ChallengesCommand Challenges { get; init; } = new(); 10 | 11 | [Command("summary", Description = "Summary of farming information")] 12 | public SummaryCommand Summary { get; init; } = new(); 13 | } 14 | -------------------------------------------------------------------------------- /src/rchia/Commands/ArgumentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace rchia.Commands; 4 | 5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 6 | public sealed class ArgumentAttribute : Attribute 7 | { 8 | public ArgumentAttribute(int index) 9 | { 10 | Index = index; 11 | } 12 | 13 | public int Index { get; } 14 | 15 | public string Name { get; init; } = string.Empty; 16 | 17 | public string? Description { get; init; } 18 | 19 | public object? Default { get; init; } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/src/rchia" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /src/rchia/Bech32/Bech32Commands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Bech32; 4 | 5 | [Command("bech32", Description = "Convert addresses to and from puzzle hashes.")] 6 | internal sealed class Bech32Commands 7 | { 8 | [Command("hash-from-address", Description = "Convert a puzzle hash to an addrees")] 9 | public HashFromAddressCommand HashFromAddress { get; init; } = new(); 10 | 11 | [Command("address-from-hash", Description = "Convert an address to a puzzle hash")] 12 | public AddressFromHashCommand AddressFromHash { get; init; } = new(); 13 | } 14 | -------------------------------------------------------------------------------- /src/rchia/Bech32/HashFromAddressCommand.cs: -------------------------------------------------------------------------------- 1 | using chia.dotnet.bech32; 2 | using rchia.Commands; 3 | 4 | namespace rchia.Bech32; 5 | 6 | internal sealed class HashFromAddressCommand : Command 7 | { 8 | [Argument(0, Name = "address", Description = "The address to convert")] 9 | public string Address { get; init; } = string.Empty; 10 | 11 | [CommandTarget] 12 | public int Run() 13 | { 14 | return DoWork("Calculating hash...", output => 15 | { 16 | output.WriteOutput("hash", Bech32M.AddressToPuzzleHash(Address), Verbose); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/rchia/Program.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Builder; 3 | using System.CommandLine.Parsing; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using rchia.Commands; 7 | 8 | namespace rchia; 9 | 10 | internal static class Program 11 | { 12 | static Program() 13 | { 14 | ClientFactory.Initialize("rchia"); 15 | } 16 | 17 | private async static Task Main(string[] args) 18 | { 19 | return await new CommandLineBuilder() 20 | .UseDefaults() 21 | .UseAttributes(Assembly.GetExecutingAssembly()) 22 | .Build() 23 | .InvokeAsync(args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rchia/Commands/ConsoleStatus.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Console; 2 | 3 | namespace rchia.Commands; 4 | 5 | public class ConsoleStatus : IStatus 6 | { 7 | private readonly StatusContext? _context; 8 | 9 | public ConsoleStatus() 10 | { 11 | } 12 | 13 | public ConsoleStatus(StatusContext? ctx) 14 | { 15 | _context = ctx; 16 | } 17 | 18 | public string Status 19 | { 20 | get => _context?.Status ?? string.Empty; 21 | set 22 | { 23 | if (_context is not null) 24 | { 25 | _context.Status = value; 26 | _context.Refresh(); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | working-directory: src 23 | - name: Build 24 | run: dotnet build --no-restore 25 | working-directory: src 26 | - name: Test 27 | run: dotnet test --no-build --verbosity normal 28 | working-directory: src 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/rchia/Services/ServicesCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Services; 4 | 5 | [Command("services", Description = "Shows the status of, start, and stop services running on the node.\nRequires a daemon endpoint.")] 6 | internal sealed class ServicesCommands 7 | { 8 | [Command("list", Description = "Show which services are running on the node")] 9 | public ListServicesCommand Services { get; init; } = new(); 10 | 11 | [Command("start", Description = "Starts a service group")] 12 | public StartServicesCommand Start { get; init; } = new(); 13 | 14 | [Command("stop", Description = "Stops a service group")] 15 | public StopServicesCommand Stop { get; init; } = new(); 16 | } 17 | -------------------------------------------------------------------------------- /src/rchia/Blocks/BlocksCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Blocks; 4 | 5 | [Command("blocks", Description = "Show informations about blocks and coins.\nRequires a daemon or full_node endpoint.")] 6 | internal sealed class BlocksCommands 7 | { 8 | [Command("block", Description = "Look up a block by height or hash")] 9 | public BlockHeaderCommand BlockHeader { get; init; } = new(); 10 | 11 | [Command("adds-and-removes", Description = "Shows additions and removals by block height or hash")] 12 | public AdditionsAndRemovalsCommand AddsAndRemoves { get; init; } = new(); 13 | 14 | [Command("recent", Description = "Show the most recent blocks")] 15 | public RecentBlocksCommand RecentBlocks { get; init; } = new(); 16 | } 17 | -------------------------------------------------------------------------------- /src/rchia/StatusMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace rchia; 4 | 5 | public sealed class StatusMessage : IDisposable 6 | { 7 | private readonly IStatus _status; 8 | private readonly string _originalMessage; 9 | 10 | public StatusMessage(IStatus status, string msg) 11 | { 12 | _status = status ?? throw new ArgumentNullException(nameof(status)); 13 | if (string.IsNullOrEmpty(msg)) 14 | { 15 | throw new ArgumentException($"{nameof(msg)} cannot be null or empty"); 16 | } 17 | 18 | _originalMessage = _status.Status; 19 | _status.Status = msg; 20 | } 21 | 22 | public void Dispose() 23 | { 24 | _status.Status = _originalMessage; 25 | GC.SuppressFinalize(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rchia/Bech32/AddressFromHashCommand.cs: -------------------------------------------------------------------------------- 1 | using chia.dotnet.bech32; 2 | using rchia.Commands; 3 | 4 | namespace rchia.Bech32; 5 | 6 | internal sealed class AddressFromHashCommand : Command 7 | { 8 | [Argument(0, Name = "hash", Description = "The hash to convert")] 9 | public string Hash { get; init; } = string.Empty; 10 | 11 | [Option("p", "prefix", Default = "xch", ArgumentHelpName = "PREFIX", Description = "The coin prefix")] 12 | public string Prefix { get; init; } = "xch"; 13 | 14 | [CommandTarget] 15 | public int Run() 16 | { 17 | return DoWork("Calculating address...", output => 18 | { 19 | var bech = new Bech32M(Prefix); 20 | output.WriteOutput("address", bech.PuzzleHashToAddress(Hash), Verbose); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/ShowEndpointCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using rchia.Commands; 3 | 4 | namespace rchia.Endpoints; 5 | 6 | internal sealed class ShowEndpointCommand : Command 7 | { 8 | [Argument(0, Name = "id", Description = "The id of the endpoint to show")] 9 | public string Id { get; init; } = string.Empty; 10 | 11 | [CommandTarget] 12 | public int Run() 13 | { 14 | return DoWork("Shoeinh endpoint...", output => 15 | { 16 | var library = EndpointLibrary.OpenLibrary(); 17 | 18 | if (!library.Endpoints.ContainsKey(Id)) 19 | { 20 | throw new InvalidOperationException($"There is no saved endpoint with an id of {Id}."); 21 | } 22 | 23 | output.WriteOutput(library.Endpoints[Id]); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rchia/Node/VersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Node; 7 | 8 | internal sealed class VersionCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Retrieving chia version...", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 16 | var proxy = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 19 | var version = await proxy.GetVersion(cts.Token); 20 | 21 | output.WriteOutput("version", version, Verbose); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rchia/Node/StopNodeCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Node; 7 | 8 | internal sealed class StopNodeCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Shutting down the node...", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 16 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 19 | await proxy.StopNode(cts.Token); 20 | 21 | output.WriteOutput("stopped", rpcClient.Endpoint.Uri, true); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rchia/Plots/RefreshPlotsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Plots; 7 | 8 | internal sealed class RefreshPlotsCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Refreshing plot list...", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Harvester); 16 | var proxy = new HarvesterProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 19 | await proxy.RefreshPlots(cts.Token); 20 | 21 | output.WriteOutput("refreshed", true, true); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rchia/Connections/ConnectionsCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Connections; 4 | 5 | [Command("connections", Description = "Various methods for managing node connections.\nRequires a daemon or full_node endpoint.")] 6 | internal sealed class BlocksCommands 7 | { 8 | [Command("add", Description = "Connect to another Full Node by ip:port")] 9 | public AddConnectionCommand Add { get; init; } = new(); 10 | 11 | [Command("list", Description = "List nodes connected to this Full Node")] 12 | public ListConnectionsCommand Connections { get; init; } = new(); 13 | 14 | [Command("prune", Description = "Prune stale connections")] 15 | public PruneConnectionsCommand Prune { get; init; } = new(); 16 | 17 | [Command("remove", Description = "Remove a Node by the full or first 8 characters of NodeID")] 18 | public RemoveConnectionCommand Remove { get; init; } = new(); 19 | } 20 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/RemoveEndpointCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using rchia.Commands; 3 | 4 | namespace rchia.Endpoints; 5 | 6 | internal sealed class RemoveEndpointCommand : Command 7 | { 8 | [Argument(0, Name = "id", Description = "The id of the endpoint to remove")] 9 | public string Id { get; init; } = string.Empty; 10 | 11 | [CommandTarget] 12 | public int Run() 13 | { 14 | return DoWork("Removing endpoint...", output => 15 | { 16 | var library = EndpointLibrary.OpenLibrary(); 17 | 18 | if (!library.Endpoints.ContainsKey(Id)) 19 | { 20 | throw new InvalidOperationException($"There is no saved endpoint with an id of {Id}."); 21 | } 22 | 23 | _ = library.Endpoints.Remove(Id); 24 | library.Save(); 25 | 26 | output.WriteOutput("removed", Id, true); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rchia/Node/NodeCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Node; 4 | 5 | [Command("node", Description = "Commands for managing a node.\nRequires a daemon endpoint.")] 6 | internal sealed class NodeCommands 7 | { 8 | [Command("netspace", Description = "Show the netspace")] 9 | public NetspaceCommand Netspace { get; init; } = new(); 10 | 11 | [Command("ping", Description = "Pings the daemon")] 12 | public PingCommand Ping { get; init; } = new(); 13 | 14 | [Command("version", Description = "Shows the version of the node")] 15 | public VersionCommand Version { get; init; } = new(); 16 | 17 | [Command("stop", Description = "Stops the node")] 18 | public StopNodeCommand Remove { get; init; } = new(); 19 | 20 | [Command("status", Description = "Show the current status of the node's view of the blockchain")] 21 | public StatusCommand Status { get; init; } = new(); 22 | } 23 | -------------------------------------------------------------------------------- /src/rchia/Node/PingCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Node 7 | { 8 | internal sealed class PingCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Pinging the daemon...", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 16 | var proxy = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | var stopWatch = Stopwatch.StartNew(); 19 | await proxy.Ping(); 20 | stopWatch.Stop(); 21 | 22 | output.WriteOutput("response_time", $"{stopWatch.ElapsedMilliseconds / 1000.0:N2}", true); 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/ListEndpointsCommand.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Endpoints; 4 | 5 | internal sealed class ListEndpointsCommand : Command 6 | { 7 | [CommandTarget] 8 | public int Run() 9 | { 10 | return DoWork("Listing saved endpoints...", output => 11 | { 12 | var library = EndpointLibrary.OpenLibrary(); 13 | foreach (var endpoint in library.Endpoints.Values) 14 | { 15 | var isDefault = endpoint.IsDefault ? "[wheat1](default)[/]" : string.Empty; 16 | output.WriteMarkupLine($" - {endpoint.Id} {isDefault}"); 17 | } 18 | output.WriteMarkupLine($"[wheat1]{library.Endpoints.Count}[/] saved endpoint{(library.Endpoints.Count == 1 ? string.Empty : "s")}"); 19 | 20 | if (Json) 21 | { 22 | output.WriteOutput(library.Endpoints); 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rchia/Commands/OptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace rchia.Commands; 4 | 5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 6 | public sealed class OptionAttribute : Attribute 7 | { 8 | public OptionAttribute(string? shortName, string? longName = null) 9 | { 10 | if (shortName is null && longName is null) 11 | { 12 | throw new InvalidOperationException($"Both {nameof(shortName)} and {nameof(longName)} cannot be null"); 13 | } 14 | 15 | ShortName = shortName; 16 | LongName = longName; 17 | } 18 | 19 | public string? LongName { get; init; } 20 | 21 | public string? ShortName { get; init; } 22 | 23 | public string? Description { get; init; } 24 | 25 | public string? ArgumentHelpName { get; init; } 26 | 27 | public bool IsRequired { get; init; } 28 | 29 | public bool IsHidden { get; init; } 30 | 31 | public object? Default { get; init; } 32 | } 33 | -------------------------------------------------------------------------------- /src/rchia/ICommandOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Spectre.Console; 4 | 5 | namespace rchia; 6 | 7 | public interface ICommandOutput 8 | { 9 | bool Verbose { get; init; } 10 | 11 | ICommandOutput SetContext(StatusContext? context); 12 | 13 | IStatus Status { get; } 14 | 15 | bool Confirm(string warning, bool force); 16 | 17 | void WriteMarkupLine(string msg); 18 | void WriteMessage(string msg, bool important = false); 19 | void WriteWarning(string msg); 20 | void WriteLine(string msg); 21 | void WriteError(Exception e); 22 | 23 | void WriteOutput(string name, object? value, bool verbose); 24 | void WriteOutput(object output); 25 | void WriteOutput(IEnumerable> output); 26 | void WriteOutput(IEnumerable output); 27 | void WriteOutput(IDictionary output); 28 | void WriteOutput(IDictionary>> output); 29 | } 30 | -------------------------------------------------------------------------------- /src/rchia/Keys/GenerateKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Keys; 7 | 8 | internal sealed class GenerateKeyCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Generating a new key...", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 16 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 19 | var mnemonic = await proxy.GenerateMnemonic(cts.Token); 20 | var fingerprint = await proxy.AddKey(mnemonic, true, cts.Token); 21 | 22 | output.WriteOutput("fingerprint", new Formattable(fingerprint, fp => $"{fp}"), Verbose); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | "${workspaceFolder}/src/rchia.sln", 13 | // Ask dotnet build to generate full paths for file names. 14 | "/property:GenerateFullPaths=true", 15 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 16 | "/consoleloggerparameters:NoSummary" 17 | ], 18 | "group":{ 19 | "kind": "build", 20 | "isDefault": true 21 | }, 22 | "presentation": { 23 | "reveal": "silent" 24 | }, 25 | "problemMatcher": "$msCompile" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/rchia/Connections/RemoveConnectionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Connections; 7 | 8 | internal sealed class RemoveConnectionCommand : EndpointOptions 9 | { 10 | [Argument(0, Name = "ID", Description = "The full or first 8 characters of NodeID")] 11 | public string Id { get; init; } = string.Empty; 12 | 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Removing connection...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 19 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | await proxy.CloseConnection(Id, cts.Token); 23 | 24 | output.WriteOutput("removed", Id, Verbose); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/SetDefaultEndpointCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using rchia.Commands; 3 | 4 | namespace rchia.Endpoints; 5 | 6 | internal sealed class SetDefaultEndpointCommand : Command 7 | { 8 | [Argument(0, Name = "id", Description = "The id of the endpoint to show")] 9 | public string Id { get; init; } = string.Empty; 10 | 11 | [CommandTarget] 12 | public int Run() 13 | { 14 | return DoWork("Setting default endpoint...", output => 15 | { 16 | var library = EndpointLibrary.OpenLibrary(); 17 | 18 | if (!library.Endpoints.ContainsKey(Id)) 19 | { 20 | throw new InvalidOperationException($"There is no saved endpoint with an id of {Id}."); 21 | } 22 | 23 | foreach (var endpoint in library.Endpoints.Values) 24 | { 25 | endpoint.IsDefault = endpoint.Id == Id; 26 | } 27 | 28 | library.Save(); 29 | output.WriteOutput("default_endpoint", Id, true); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/rchia/Plots/AddPlotsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Plots; 7 | 8 | internal sealed class AddPlotsCommand : EndpointOptions 9 | { 10 | [Argument(0, Name = "finalDir", Default = ".", Description = "Final directory for plots (relative or absolute)")] 11 | public string FinalDir { get; init; } = "."; 12 | 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Adding plot directory...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Harvester); 19 | var proxy = new HarvesterProxy(rpcClient, ClientFactory.Factory.OriginService); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | await proxy.AddPlotDirectory(FinalDir, cts.Token); 23 | 24 | output.WriteOutput("added", FinalDir, Verbose); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rchia/Plots/RemovePlotsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Plots; 7 | 8 | internal sealed class RemovePlotsCommand : EndpointOptions 9 | { 10 | [Argument(0, Name = "finalDir", Default = ".", Description = "Final directory for plots (relative or absolute)")] 11 | public string FinalDir { get; init; } = "."; 12 | 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Removing plot directory...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Harvester); 19 | var proxy = new HarvesterProxy(rpcClient, ClientFactory.Factory.OriginService); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | await proxy.RemovePlotDirectory(FinalDir, cts.Token); 23 | 24 | output.WriteOutput("removed", FinalDir, Verbose); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/EndpointsCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Endpoints; 4 | 5 | [Command("endpoints", Description = "Manage saved endpoints.")] 6 | internal sealed class EndpointsCommands 7 | { 8 | [Command("add", Description = "Saves a new endpoint")] 9 | public AddEndpointCommand Add { get; init; } = new(); 10 | 11 | [Command("list", Description = "Lists the ids of saved endpoints")] 12 | public ListEndpointsCommand List { get; init; } = new(); 13 | 14 | [Command("remove", Description = "Removes a saved endpoint")] 15 | public RemoveEndpointCommand Remove { get; init; } = new(); 16 | 17 | [Command("show", Description = "Shows the details of a saved endpoint")] 18 | public ShowEndpointCommand Show { get; init; } = new(); 19 | 20 | [Command("set-default", Description = "Sets the endpoint to be the default for --default-endpoint")] 21 | public SetDefaultEndpointCommand SetDefault { get; init; } = new(); 22 | 23 | [Command("test", Description = "Test the connection to a saved endpoint")] 24 | public TestEndpointCommand Test { get; init; } = new(); 25 | } 26 | -------------------------------------------------------------------------------- /src/rchia/Keys/GenerateAndPrintKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Keys; 7 | 8 | internal sealed class GenerateAndPrintKeyCommand : EndpointOptions 9 | { 10 | [CommandTarget] 11 | public async Task Run() 12 | { 13 | return await DoWorkAsync("Generating a new key....", async output => 14 | { 15 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 16 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 17 | 18 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 19 | var mnemonic = await proxy.GenerateMnemonic(cts.Token); 20 | 21 | output.WriteLine("Generated private key. Mnemonic (24 secret words):"); 22 | output.WriteOutput(mnemonic); 23 | output.WriteMarkupLine($"Note that this key has not been added to the keychain. Run '[grey]rchia keys add {string.Join(' ', mnemonic)}[/]' to do so."); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rchia/Keys/KeysCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Keys; 4 | 5 | [Command("keys", Description = "Manage your keys\nRequires a wallet or daemon endpoint.")] 6 | internal sealed class KeysCommands 7 | { 8 | [Command("add", Description = "Add a private key by mnemonic")] 9 | public AddKeyCommand Add { get; init; } = new(); 10 | 11 | [Command("delete", Description = "Delete a key by its pk fingerprint in hex form")] 12 | public DeleteKeyCommand Delete { get; init; } = new(); 13 | 14 | [Command("generate-and-print", Description = "Generates but does NOT add to keychain")] 15 | public GenerateAndPrintKeyCommand GenerateAndPrint { get; init; } = new(); 16 | 17 | [Command("generate", Description = "Generates and adds a key to keychain")] 18 | public GenerateKeyCommand Generate { get; init; } = new(); 19 | 20 | [Command("show", Description = "Displays all the keys in keychain")] 21 | public ShowKeysCommand Show { get; init; } = new(); 22 | 23 | [Command("delete-all", Description = "Delete all private keys in keychain")] 24 | public DeleteAllKeys DeleteAll { get; init; } = new(); 25 | } 26 | -------------------------------------------------------------------------------- /src/rchia/Wallets/GetAddressCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Wallet; 7 | 8 | internal sealed class GetAddressCommand : WalletCommand 9 | { 10 | [Option("n", "new", Description = "Flag indicating whether to create a new address")] 11 | public bool New { get; init; } 12 | 13 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 14 | public uint Id { get; init; } = 1; 15 | 16 | [CommandTarget] 17 | public async Task Run() 18 | { 19 | return await DoWorkAsync("Retrieving wallet address...", async output => 20 | { 21 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 22 | var wallet = new chia.dotnet.Wallet(Id, await Login(rpcClient, output)); 23 | 24 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 25 | var address = await wallet.GetNextAddress(New, cts.Token); 26 | 27 | output.WriteOutput("address", address, Verbose); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/rchia/Wallets/DeleteUnconfirmedTransactionsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Wallet; 7 | 8 | internal sealed class DeleteUnconfirmedTransactionsCommand : WalletCommand 9 | { 10 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 11 | public uint Id { get; init; } = 1; 12 | 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Deleting unconfirmed transactions...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 19 | var wallet = new chia.dotnet.Wallet(Id, await Login(rpcClient, output)); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | await wallet.DeleteUnconfirmedTransactions(cts.Token); 23 | 24 | output.WriteMarkupLine($"Successfully deleted all unconfirmed transactions for wallet id:"); 25 | output.WriteOutput("wallet", Id, Verbose); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/rchia/Connections/AddConnectionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Connections; 8 | 9 | internal sealed class AddConnectionCommand : EndpointOptions 10 | { 11 | [Argument(0, Name = "host", Description = "Full Node ip:port")] 12 | public string Host { get; init; } = string.Empty; 13 | 14 | [CommandTarget] 15 | public async Task Run() 16 | { 17 | return await DoWorkAsync("Adding connection...", async output => 18 | { 19 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 20 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 21 | 22 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 23 | var uri = Host.StartsWith("http") ? new Uri(Host) : new Uri("https://" + Host); // need to add a scheme so uri can be parsed 24 | await proxy.OpenConnection(uri.Host, uri.Port, cts.Token); 25 | 26 | output.WriteOutput("added", uri, Verbose); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/TestEndpointCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using rchia.Commands; 4 | 5 | namespace rchia.Endpoints; 6 | 7 | internal sealed class TestEndpointCommand : Command 8 | { 9 | [Argument(0, Name = "id", Description = "The id of the endpoint to test")] 10 | public string Id { get; init; } = string.Empty; 11 | 12 | [Option("to", "timeout", Default = 30, ArgumentHelpName = "TIMEOUT", Description = "Timeout in seconds")] 13 | public int Timeout { get; init; } = 30; 14 | 15 | [CommandTarget] 16 | public async Task Run() 17 | { 18 | return await DoWorkAsync("Testing connection...", async output => 19 | { 20 | var library = EndpointLibrary.OpenLibrary(); 21 | 22 | if (!library.Endpoints.ContainsKey(Id)) 23 | { 24 | throw new InvalidCastException($"There is no saved endpoint with an id of {Id}."); 25 | } 26 | var endpoint = library.Endpoints[Id]; 27 | await ClientFactory.Factory.TestConnection(endpoint.EndpointInfo, Timeout * 1000); 28 | 29 | output.WriteOutput("connected_to", Id, true); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/rchia/Wallets/WalletsCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Wallet; 4 | 5 | [Command("wallets", Description = "Manage your wallet.\nRequires a wallet or daemon endpoint.")] 6 | internal sealed class WalletsCommands 7 | { 8 | [Command("show", Description = "Show wallet information")] 9 | public ShowWalletCommand Show { get; init; } = new(); 10 | 11 | [Command("delete-unconfirmed-transactions", Description = "Deletes all unconfirmed transactions from a wallet")] 12 | public DeleteUnconfirmedTransactionsCommand DeleteUnconfirmedTransactions { get; init; } = new(); 13 | 14 | [Command("get-address", Description = "Get a wallet receive address")] 15 | public GetAddressCommand GetAddress { get; init; } = new(); 16 | 17 | [Command("get-transaction", Description = "Show a transaction")] 18 | public GetTransactionCommand GetTransaction { get; init; } = new(); 19 | 20 | [Command("list-transactions", Description = "List all transactions")] 21 | public ListTransactionsCommand ListTransactions { get; init; } = new(); 22 | 23 | [Command("send", Description = "Send chia to another wallet")] 24 | public SendTransactionCommand Send { get; init; } = new(); 25 | } 26 | -------------------------------------------------------------------------------- /src/rchia/Plots/ShowDirectoriesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Plots; 8 | 9 | internal sealed class ShowDirectoriesCommand : EndpointOptions 10 | { 11 | [CommandTarget] 12 | public async Task Run() 13 | { 14 | return await DoWorkAsync("Retrieving plot info...", async output => 15 | { 16 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Harvester); 17 | var proxy = new HarvesterProxy(rpcClient, ClientFactory.Factory.OriginService); 18 | 19 | output.WriteLine("Directories where plots are being searched for:"); 20 | output.WriteMessage("Note that subdirectories must be added manually", false); 21 | output.WriteMarkupLine("Add with '[grey]chia plots add [/]' and remove with '[grey]chia plots remove [/]'"); 22 | 23 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 24 | var dirs = from path in await proxy.GetPlotDirectories(cts.Token) 25 | select path; 26 | output.WriteOutput(dirs); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/PlotNftCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.PlotNft; 4 | 5 | [Command("plotnft", Description = "Manage your plot NFTs.\nRequires a wallet or daemon endpoint.")] 6 | internal sealed class PlotNftCommands 7 | { 8 | [Command("claim", Description = "Claim rewards from a plot NFT")] 9 | public ClaimNftCommand Claim { get; init; } = new(); 10 | 11 | [Command("create", Description = "Create a plot NFT")] 12 | public CreatePlotNftCommand Create { get; init; } = new(); 13 | 14 | [Command("get-login-link", Description = "Create a login link for a pool. To get the launcher id, use 'plotnft show'.")] 15 | public GetLoginLinkCommand GetLoginLink { get; init; } = new(); 16 | 17 | [Command("inspect", Description = "Get Detailed plotnft information as JSON")] 18 | public InspectNftCommand Inspect { get; init; } = new(); 19 | 20 | [Command("join", Description = "Join a plot NFT to a Pool")] 21 | public JoinPoolCommand Join { get; init; } = new(); 22 | 23 | [Command("leave", Description = "Leave a pool and return to self-farming")] 24 | public LeavePoolCommand Leave { get; init; } = new(); 25 | 26 | [Command("show", Description = "Show plotnft information")] 27 | public ShowPlotNftCommand Show { get; init; } = new(); 28 | } 29 | -------------------------------------------------------------------------------- /src/rchia/Keys/DeleteAllKeysCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Keys; 7 | 8 | internal sealed class DeleteAllKeys : EndpointOptions 9 | { 10 | [Option("f", "force", Description = "Delete all keys without prompting for confirmation")] 11 | public bool Force { get; init; } 12 | 13 | protected async override Task Confirm(ICommandOutput output) 14 | { 15 | await Task.CompletedTask; 16 | 17 | return output.Confirm($"Deleting all of your keys [wheat1]CANNOT[/] be undone.\nAre you sure you want to delete all keys?", Force); 18 | } 19 | 20 | [CommandTarget] 21 | public async Task Run() 22 | { 23 | return await DoWorkAsync("Deleting all keys....", async output => 24 | { 25 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 26 | 27 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 28 | 29 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 30 | await proxy.DeleteAllKeys(cts.Token); 31 | 32 | output.WriteOutput("status", "Deleted all keys", Verbose); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ main ] 9 | schedule: 10 | - cron: '44 20 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'csharp' ] 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | - name: Setup .NET 36 | uses: actions/setup-dotnet@v1 37 | with: 38 | dotnet-version: 6.0.x 39 | 40 | - name: Restore dependencies 41 | run: dotnet restore 42 | working-directory: src 43 | 44 | - name: Build 45 | run: dotnet build --no-restore /p:UseSharedCompilation=false 46 | working-directory: src 47 | 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@v1 50 | -------------------------------------------------------------------------------- /src/rchia/Services/ServiceGroups.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace rchia.Services; 4 | 5 | internal static class ServiceGroups 6 | { 7 | static ServiceGroups() 8 | { 9 | Groups = new Dictionary>() 10 | { 11 | { "all", "chia_harvester chia_timelord_launcher chia_timelord chia_farmer chia_full_node chia_wallet".Split(' ')}, 12 | { "node", "chia_full_node".Split(' ')}, 13 | { "harvester", "chia_harvester".Split(' ')}, 14 | { "farmer", "chia_harvester chia_farmer chia_full_node chia_wallet".Split(' ')}, 15 | { "farmer-no-wallet", "chia_harvester chia_farmer chia_full_node".Split(' ')}, 16 | { "farmer-only", "chia_farmer".Split(' ')}, 17 | { "timelord", "chia_timelord_launcher chia_timelord chia_full_node".Split(' ')}, 18 | { "timelord-only", "chia_timelord".Split(' ')}, 19 | { "timelord-launcher-only", "chia_timelord_launcher".Split(' ')}, 20 | { "wallet", "chia_wallet chia_full_node".Split(' ')}, 21 | { "wallet-only", "chia_wallet".Split(' ')}, 22 | { "introducer", "chia_introducer".Split(' ')}, 23 | { "simulator", "chia_full_node_simulator".Split(' ')} 24 | }; 25 | } 26 | 27 | public static IReadOnlyDictionary> Groups { get; private set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/rchia/Wallets/GetTransactionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Wallet; 8 | 9 | internal sealed class GetTransactionCommand : WalletCommand 10 | { 11 | [Argument(0, Name = "TxId", Description = "Transaction id to search for")] 12 | public string TxId { get; init; } = string.Empty; 13 | 14 | [CommandTarget] 15 | public async Task Run() 16 | { 17 | return await DoWorkAsync("Retrieving transaction...", async output => 18 | { 19 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 20 | var proxy = await Login(rpcClient, output); 21 | 22 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 23 | var tx = await proxy.GetTransaction(TxId, cts.Token); 24 | 25 | if (Json) 26 | { 27 | output.WriteOutput(tx); 28 | } 29 | else 30 | { 31 | var (NetworkName, NetworkPrefix) = await proxy.GetNetworkInfo(cts.Token); 32 | var result = new Dictionary(); 33 | FormatTransaction(tx, NetworkPrefix, result); 34 | output.WriteOutput(result); 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/InspectNftCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.PlotNft; 7 | 8 | internal sealed class InspectNftCommand : WalletCommand 9 | { 10 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 11 | public uint Id { get; init; } = 1; 12 | 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Retrieving nft plot info...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 19 | var wallet = new PoolWallet(Id, await Login(rpcClient, output)); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | await wallet.Validate(cts.Token); 23 | 24 | var result = await wallet.Status(cts.Token); 25 | 26 | if (Json) 27 | { 28 | output.WriteOutput(result); 29 | } 30 | else 31 | { 32 | output.WriteOutput("launch_id", result.State.LauncherId, Verbose); 33 | 34 | foreach (var tx in result.UnconfirmedTransactions) 35 | { 36 | PrintTransactionSentTo(output, tx); 37 | } 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/GetLoginLinkCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.PlotNft; 8 | 9 | internal sealed class GetLoginLinkCommand : EndpointOptions 10 | { 11 | [Argument(0, Name = "launcherId", Description = "Launcher ID of the plotnft")] 12 | public string LauncherId { get; init; } = string.Empty; 13 | 14 | [CommandTarget] 15 | public async Task Run() 16 | { 17 | return await DoWorkAsync("Getting pool login link...", async output => 18 | { 19 | if (string.IsNullOrEmpty(LauncherId)) 20 | { 21 | throw new InvalidOperationException("A valid launcher id is required. To get the launcher id, use 'plotnft show'."); 22 | } 23 | 24 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Farmer); 25 | var farmer = new FarmerProxy(rpcClient, ClientFactory.Factory.OriginService); 26 | 27 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 28 | var link = await farmer.GetPoolLoginLink(LauncherId, cts.Token); 29 | 30 | if (string.IsNullOrEmpty(link)) 31 | { 32 | output.WriteWarning("Was not able to get login link."); 33 | } 34 | 35 | output.WriteOutput("link", link, Verbose); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/rchia/Plots/ListPlottersCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Plots 9 | { 10 | internal sealed class ListPlottersCommand : EndpointOptions 11 | { 12 | [CommandTarget] 13 | public async Task Run() 14 | { 15 | return await DoWorkAsync("Adding plot directory...", async output => 16 | { 17 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 18 | var proxy = new PlotterProxy(rpcClient, ClientFactory.Factory.OriginService); 19 | 20 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 21 | var plotters = await proxy.GetPlotters(cts.Token); 22 | 23 | var table = from plotter in plotters.Values 24 | select new Dictionary() 25 | { 26 | { "name", plotter.DisplayName }, 27 | { "installed", plotter.Installed }, 28 | { "can_install", plotter.CanInstall }, 29 | { "version", plotter.Version ?? string.Empty } 30 | }; 31 | 32 | output.WriteOutput(table); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/rchia.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "rchia", "rchia\rchia.csproj", "{8AE515F5-F6DC-40F0-94BE-42F06FB16255}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8B8712F4-250C-4205-B655-E44A1FE09F26}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | publish-all.ps1 = publish-all.ps1 12 | test.ps1 = test.ps1 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {8AE515F5-F6DC-40F0-94BE-42F06FB16255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {8AE515F5-F6DC-40F0-94BE-42F06FB16255}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {8AE515F5-F6DC-40F0-94BE-42F06FB16255}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {8AE515F5-F6DC-40F0-94BE-42F06FB16255}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(ExtensibilityGlobals) = postSolution 30 | SolutionGuid = {C63A9946-7E72-44BB-906A-86CFE55A7F36} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /src/test.ps1: -------------------------------------------------------------------------------- 1 | ./rchia bech32 address-from-hash 0xdb96f26f5245baec3c4e06da21d9d50c8718f9920b5e9354c3a936fc0ee49a66 2 | ./rchia bech32 hash-from-address xch1mwt0ym6jgkawc0zwqmdzrkw4pjr337vjpd0fx4xr4ym0crhynfnq96pztp 3 | ./rchia blocks block -h 100 -ep wsl 4 | ./rchia blocks recent -ep wsl 5 | ./rchia blocks adds-and-removes -h 100 -ep wsl 6 | ./rchia connections list -ep wsl 7 | ./rchia connections prune -ep wsl 8 | ./rchia endpoints add example https://example.com c:\example.crt c:\example.key 9 | ./rchia endpoints set-default example 10 | ./rchia endpoints show example 11 | ./rchia endpoints list 12 | ./rchia endpoints test example 13 | ./rchia endpoints remove example 14 | ./rchia farm challenges -ep wsl 15 | ./rchia farm summary -ep wsl 16 | ./rchia keys show -ep wsl 17 | ./rchia keys generate-and-print -ep wsl 18 | ./rchia node netspace -ep wsl 19 | ./rchia node ping -ep wsl 20 | ./rchia node version -ep wsl 21 | ./rchia node status -ep wsl 22 | ./rchia node stop -ep wsl 23 | ./rchia plotnft get-login-link -ep wsl -l 0x... 24 | ./rchia plotnft inspect -ep wsl -i 2 25 | ./rchia plotnft show -ep wsl -i 2 26 | ./rchia plots list -ep wsl 27 | ./rchia plots directories -ep wsl 28 | ./rchia plots plotters -ep wsl 29 | ./rchia plots queue -ep wsl 30 | ./rchia plots log -ep wsl 31 | ./rchia services list -ep wsl 32 | ./rchia wallets get-address -ep wsl 33 | ./rchia wallets get-transaction -tx 0x52fe2f8d65f1ca511a4d46c7fc0f4a7dfdac64af5df6caea8ee1cff18b8c3829 -ep wsl 34 | ./rchia wallets list-transactions -ep wsl 35 | ./rchia wallets show -ep wsl 36 | -------------------------------------------------------------------------------- /src/rchia/Services/ListServicesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using rchia.Commands; 8 | 9 | namespace rchia.Services; 10 | 11 | internal sealed class ListServicesCommand : EndpointOptions 12 | { 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Retrieving service info...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 19 | var proxy = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 20 | 21 | var fields = typeof(ServiceNames).GetFields(BindingFlags.Public | BindingFlags.Static); 22 | var serviceNames = new ServiceNames(); 23 | 24 | var result = new Dictionary(); 25 | foreach (var name in fields.Where(f => f.Name != "Daemon")) 26 | { 27 | var service = name.GetValue(serviceNames)?.ToString() ?? string.Empty; 28 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 29 | 30 | var isRunning = await proxy.IsServiceRunning(service, cts.Token); 31 | result.Add(service, isRunning ? new Formattable("running", "green") : new Formattable("not running", "grey")); 32 | } 33 | 34 | output.WriteOutput(result); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rchia/Keys/DeleteKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Keys; 8 | 9 | internal sealed class DeleteKeyCommand : WalletCommand 10 | { 11 | [Option("f", "force", Description = "Delete the key without prompting for confirmation")] 12 | public bool Force { get; init; } 13 | 14 | protected async override Task Confirm(ICommandOutput output) 15 | { 16 | await Task.CompletedTask; 17 | 18 | return output.Confirm($"Deleting a key CANNOT be undone.\nAre you sure you want to delete key {Fingerprint}?", Force); 19 | } 20 | 21 | [CommandTarget] 22 | public async Task Run() 23 | { 24 | return await DoWorkAsync("Deleting key...", async output => 25 | { 26 | if (Fingerprint is null || Fingerprint == 0) 27 | { 28 | throw new InvalidOperationException($"{Fingerprint} is not a valid wallet fingerprint"); 29 | } 30 | 31 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Farmer); 32 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 33 | 34 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 35 | await proxy.DeleteKey(Fingerprint.Value, cts.Token); 36 | 37 | output.WriteOutput("deleted", new Formattable(Fingerprint.Value, fp => $"{fp}"), Verbose); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/LeavePoolCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.PlotNft; 7 | 8 | internal sealed class LeavePoolCommand : WalletCommand 9 | { 10 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 11 | public uint Id { get; init; } = 1; 12 | 13 | [Option("f", "force", Description = "Do not prompt before nft creation")] 14 | public bool Force { get; init; } 15 | 16 | protected async override Task Confirm(ICommandOutput output) 17 | { 18 | await Task.CompletedTask; 19 | 20 | return output.Confirm($"Are you sure you want to start self-farming with Plot NFT on wallet id {Id}?", Force); 21 | } 22 | 23 | [CommandTarget] 24 | public async Task Run() 25 | { 26 | return await DoWorkAsync("Leaving pool...", async output => 27 | { 28 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 29 | var wallet = new PoolWallet(Id, await Login(rpcClient, output)); 30 | 31 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 32 | await wallet.Validate(cts.Token); 33 | 34 | var tx = await wallet.SelfPool(cts.Token); 35 | 36 | if (Json) 37 | { 38 | output.WriteOutput(tx); 39 | } 40 | else 41 | { 42 | PrintTransactionSentTo(output, tx); 43 | } 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rchia/Plots/PlotsCommands.cs: -------------------------------------------------------------------------------- 1 | using rchia.Commands; 2 | 3 | namespace rchia.Plots; 4 | 5 | [Command("plots", Description = "Manage plots and plot directories.\nRequires a harvester, plotter or daemon endpoint.")] 6 | internal sealed class PlotsCommands 7 | { 8 | [Command("add", Description = "Adds a directory of plots to config.yaml")] 9 | public AddPlotsCommand Add { get; init; } = new(); 10 | 11 | [Command("create", Description = "Create plots")] 12 | public CreatePlotsCommand Create { get; init; } = new(); 13 | 14 | [Command("list", Description = "List plots on the node")] 15 | public ListPlotsCommand List { get; init; } = new(); 16 | 17 | [Command("log", Description = "View the log for running plot jobs or a specific plot")] 18 | public ShowPlotLogCommand Log { get; init; } = new(); 19 | 20 | [Command("queue", Description = "View the plot queue")] 21 | public ShowPlotQueueCommand Queue { get; init; } = new(); 22 | 23 | [Command("refresh", Description = "Refreshes the harvester's plot list")] 24 | public RefreshPlotsCommand Refresh { get; init; } = new(); 25 | 26 | [Command("remove", Description = "Removes a directory of plots from config.yaml")] 27 | public RemovePlotsCommand Remove { get; init; } = new(); 28 | 29 | [Command("directories", Description = "Shows the current plot directories")] 30 | public ShowDirectoriesCommand Show { get; init; } = new(); 31 | 32 | [Command("plotters", Description = "Lists installed and installable plotters")] 33 | public ListPlottersCommand Plotters { get; init; } = new(); 34 | } 35 | -------------------------------------------------------------------------------- /src/rchia/EndpointOptions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using rchia.Commands; 3 | 4 | namespace rchia; 5 | 6 | public abstract class EndpointOptions : Command 7 | { 8 | [Option("uri", "endpoint-uri", ArgumentHelpName = "URI", Description = "The uri of the rpc endpoint, including the proper port and wss/https scheme prefix")] 9 | public string? EndpointUri { get; init; } 10 | 11 | [Option("cp", "cert-path", ArgumentHelpName = "PATH", Description = "The full path to the .crt file to use for authentication")] 12 | public FileInfo? CertPath { get; init; } 13 | 14 | [Option("kp", "key-path", ArgumentHelpName = "PATH", Description = "The full path to the .key file to use for authentication")] 15 | public FileInfo? KeyPath { get; init; } 16 | 17 | [Option("ccp", "chia-config-path", ArgumentHelpName = "PATH", Description = "The full path to a chia config yaml file for endpoints")] 18 | public FileInfo? ConfigPath { get; init; } 19 | 20 | [Option("dcc", "default-chia-config", Description = "Flag indicating to use the default chia config for endpoints")] 21 | public bool DefaultConfig { get; init; } 22 | 23 | [Option("de", "default-endpoint", Description = "Flag indicating to use the default saved endpoint")] 24 | public bool DefaultEndpoint { get; init; } 25 | 26 | [Option("ep", "endpoint", ArgumentHelpName = "ID", Description = "Use a saved endpoint")] 27 | public string? Endpoint { get; init; } 28 | 29 | [Option("to", "timeout", Default = 30, ArgumentHelpName = "TIMEOUT", Description = "Timeout in seconds", IsHidden = true)] 30 | public int Timeout { get; init; } = 30; 31 | 32 | internal int TimeoutMilliseconds => Timeout * 1000; 33 | } 34 | -------------------------------------------------------------------------------- /src/rchia/Formattable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace rchia; 5 | 6 | internal interface IHasRawValue 7 | { 8 | object? GetRawValue(); 9 | } 10 | 11 | /// 12 | /// small helper to allow human readabke formatting of values for console output 13 | /// Just serializes as a string in json 14 | /// 15 | [JsonConverter(typeof(RawValueConverter))] 16 | internal sealed class Formattable : IHasRawValue 17 | { 18 | private readonly Func _formatter; 19 | 20 | public Formattable(T value, string color) 21 | : this(value, v => $"[{color}]{v}[/]") 22 | { 23 | } 24 | 25 | public Formattable(T value, Func formatter) 26 | { 27 | Value = value; 28 | _formatter = formatter; 29 | } 30 | 31 | public T Value { get; init; } 32 | 33 | public object? GetRawValue() 34 | { 35 | return Value; 36 | } 37 | 38 | public override string? ToString() 39 | { 40 | return _formatter(Value); 41 | } 42 | } 43 | 44 | internal sealed class RawValueConverter : JsonConverter 45 | { 46 | public override bool CanConvert(Type objectType) 47 | { 48 | return true; 49 | } 50 | 51 | public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 52 | { 53 | throw new NotImplementedException(); 54 | } 55 | 56 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 57 | { 58 | if (value is not IHasRawValue v) 59 | { 60 | writer.WriteNull(); 61 | } 62 | else 63 | { 64 | writer.WriteValue(v.GetRawValue()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/ClaimNftCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.PlotNft; 7 | 8 | internal sealed class ClaimNftCommand : WalletCommand 9 | { 10 | [Option("f", "force", Description = "Do not prompt before claiming rewards")] 11 | public bool Force { get; init; } 12 | 13 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 14 | public uint Id { get; init; } = 1; 15 | 16 | [Option("m", "fee", Default = 0, Description = "Set the fees for the transaction, in XCH")] 17 | public decimal Fee { get; init; } 18 | 19 | protected async override Task Confirm(ICommandOutput output) 20 | { 21 | await Task.CompletedTask; 22 | 23 | return output.Confirm($"Are you sure you want to claim rewards for wallet ID {Id}?", Force); 24 | } 25 | 26 | [CommandTarget] 27 | public async Task Run() 28 | { 29 | return await DoWorkAsync("Claiming pool rewards...", async output => 30 | { 31 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 32 | var wallet = new PoolWallet(Id, await Login(rpcClient, output)); 33 | 34 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 35 | await wallet.Validate(cts.Token); 36 | 37 | var rewards = await wallet.AbsorbRewards(Fee.ToMojo(), cts.Token); 38 | 39 | if (Json) 40 | { 41 | output.WriteOutput(rewards); 42 | } 43 | else 44 | { 45 | PrintTransactionSentTo(output, rewards.Transaction); 46 | } 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/src/rchia/bin/Debug/net5.0/rchia.dll", 13 | "args": [ 14 | "endpoints", "-a", "local-full_node" ,"https://localhost:8555", 15 | //"show", 16 | //"-c", 17 | //"-s", 18 | //"-a", "51.15.76.147:58444", 19 | //"-b", "30f64608023b418918a2acde1d3af840aa28e80a082e8a42ee6e20bda6f53dac", 20 | //"-r", "0xfc7d2090ac49c63c6f3a4411addab6dbb472e4953b65ec31227c7481a59ba26d", 21 | // "--endpoint-uri", "https://localhost:8555", 22 | // "--cert-path", "/home/don/.chia/mainnet/config/ssl/full_node/private_full_node.crt", 23 | // "--key-path", "/home/don/.chia/mainnet/config/ssl/full_node/private_full_node.key", 24 | //"--endpoint-uri", "https://172.29.106.245:8555", 25 | //"--cert-path", "\\\\wsl$\\Ubuntu-20.04\\home\\don\\.chia\\mainnet\\config\\ssl\\full_node\\private_full_node.crt", 26 | //"--key-path", "\\\\wsl$\\Ubuntu-20.04\\home\\don\\.chia\\mainnet\\config\\ssl\\full_node\\private_full_node.key", 27 | "-v" 28 | ], 29 | "cwd": "${workspaceFolder}/src/rchia", 30 | "stopAtEntry": false, 31 | "console": "internalConsole" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /src/rchia/Farm/ChallengesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Farm; 9 | 10 | internal sealed class ChallengesCommand : EndpointOptions 11 | { 12 | [Argument(0, Name = "limit", Default = 20, Description = "Limit the number of challenges shown. Use 0 to disable the limit")] 13 | public int Limit { get; init; } = 20; 14 | 15 | [CommandTarget] 16 | public async Task Run() 17 | { 18 | return await DoWorkAsync("Retrieving challenges...", async output => 19 | { 20 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Farmer); 21 | var farmer = new FarmerProxy(rpcClient, ClientFactory.Factory.OriginService); 22 | 23 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 24 | var signagePoints = await farmer.GetSignagePoints(cts.Token); 25 | 26 | var list = signagePoints.Reverse().ToList(); // convert to list to avoid multiple iteration 27 | var limit = Limit == 0 ? list.Count : Limit; 28 | 29 | var table = from sp in list.Take(limit) 30 | select new Dictionary 31 | { 32 | { "index", sp.SignagePoint.SignagePointIndex }, 33 | { "hash", new Formattable(sp.SignagePoint.ChallengeHash, hash => hash.Replace("0x", string.Empty)) } 34 | }; 35 | 36 | output.WriteOutput(table); 37 | output.WriteMessage($"Showing {table.Count()} of {signagePoints.Count()} challenge{(list.Count == 1 ? string.Empty : "s")}."); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rchia/Plots/ShowPlotQueueCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Plots; 9 | 10 | internal sealed class ShowPlotQueueCommand : EndpointOptions 11 | { 12 | [CommandTarget] 13 | public async Task Run() 14 | { 15 | return await DoWorkAsync("Retrieving plot queue...", async output => 16 | { 17 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 18 | var proxy = new PlotterProxy(rpcClient, ClientFactory.Factory.OriginService); 19 | 20 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 21 | var q = await proxy.RegisterPlotter(cts.Token); 22 | var plots = from p in q 23 | group p by p.PlotState into g 24 | select g; 25 | 26 | if (Json) 27 | { 28 | var result = from grp in plots 29 | select new Dictionary>() 30 | { 31 | { grp.Key.ToString(), grp.Select(item => item.Id) } 32 | }; 33 | 34 | output.WriteOutput(result); 35 | } 36 | else 37 | { 38 | foreach (var group in plots) 39 | { 40 | output.WriteMarkupLine($"{group.Key} {group.Count()}"); 41 | if (Verbose) 42 | { 43 | foreach (var item in group) 44 | { 45 | output.WriteLine($" {item.Id}"); 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/rchia/Keys/AddKeyCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using chia.dotnet; 8 | using rchia.Commands; 9 | 10 | namespace rchia.Keys; 11 | 12 | internal sealed class AddKeyCommand : EndpointOptions 13 | { 14 | [Argument(0, Name = "mnemonic", Description = "The 24 word mnemonic key phrase")] 15 | public List Mnemonic { get; init; } = new List(); 16 | 17 | [Option("f", "filename", Description = "A filename containing the secret key mnemonic to add")] 18 | public FileInfo? Filename { get; init; } 19 | 20 | [CommandTarget] 21 | public async Task Run() 22 | { 23 | return await DoWorkAsync("Adding key...", async output => 24 | { 25 | var mnemonic = Mnemonic; 26 | 27 | if (Filename is not null) 28 | { 29 | using var reader = Filename.OpenText(); 30 | var contents = reader.ReadToEnd(); 31 | contents = contents.Replace('\n', ' '); // this way we can have all words on one line or line per each 32 | mnemonic = contents.Split(' ').ToList(); 33 | } 34 | 35 | if (mnemonic.Count != 24) 36 | { 37 | throw new InvalidOperationException("Exactly 24 words are required in the mnenomic passphrase"); 38 | } 39 | 40 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 41 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 42 | 43 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 44 | var fingerprint = await proxy.AddKey(mnemonic, true, cts.Token); 45 | 46 | output.WriteOutput("fingerprint", new Formattable(fingerprint, fp => $"{fp}"), Verbose); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/rchia/Keys/ShowKeysCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Keys; 8 | 9 | internal sealed class ShowKeysCommand : EndpointOptions 10 | { 11 | [Option("m", "show-mnemonic-seed", Description = "Show the mnemonic seed of the keys")] 12 | public bool ShowMnemonicSeed { get; init; } 13 | 14 | [CommandTarget] 15 | public async Task Run() 16 | { 17 | return await DoWorkAsync("Retrieving keys...", async output => 18 | { 19 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 20 | var proxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 21 | 22 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 23 | var table = new List>(); 24 | foreach (var fingerprint in await proxy.GetPublicKeys(cts.Token)) 25 | { 26 | using var cts1 = new CancellationTokenSource(TimeoutMilliseconds); 27 | var (Fingerprint, Sk, Pk, FarmerPk, PoolPk, Seed) = await proxy.GetPrivateKey(fingerprint, cts1.Token); 28 | 29 | var row = new Dictionary 30 | { 31 | { "fingerprint", new Formattable(fingerprint, fp => $"{fp}") }, 32 | { "master_public_key", Pk }, 33 | { "farmer_public_key", FarmerPk }, 34 | { "pool_public_key", PoolPk } 35 | }; 36 | 37 | if (ShowMnemonicSeed) 38 | { 39 | row.Add("master_private_key", Sk); 40 | row.Add("mnemonic_seed", Seed); 41 | } 42 | 43 | table.Add(row); 44 | } 45 | 46 | output.WriteOutput(table); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/rchia/Blocks/RecentBlocksCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Linq; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Blocks; 9 | 10 | internal sealed class RecentBlocksCommand : EndpointOptions 11 | { 12 | [CommandTarget] 13 | public async Task Run() 14 | { 15 | return await DoWorkAsync("Retrieving recent blocks...", async output => 16 | { 17 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 18 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 19 | 20 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 21 | var state = await proxy.GetBlockchainState(cts.Token); 22 | 23 | var blocks = new List(); 24 | if (state.Peak is not null) 25 | { 26 | var block = await proxy.GetBlockRecord(state.Peak.HeaderHash, cts.Token); 27 | 28 | while (block is not null && blocks.Count < 10 && block.Height > 0) 29 | { 30 | blocks.Add(block); 31 | using var cts1 = new CancellationTokenSource(TimeoutMilliseconds); 32 | block = await proxy.GetBlockRecord(block.PrevHash, cts.Token); 33 | } 34 | } 35 | 36 | if (Json) 37 | { 38 | output.WriteOutput(blocks); 39 | } 40 | else 41 | { 42 | var table = from b in blocks 43 | select new Dictionary() 44 | { 45 | { "height", b.Height }, 46 | { "hash", b.HeaderHash.Replace("0x", "") } 47 | }; 48 | 49 | output.WriteOutput(table); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/rchia/Endpoints/AddEndpointCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using chia.dotnet; 4 | using rchia.Commands; 5 | 6 | namespace rchia.Endpoints; 7 | 8 | internal sealed class AddEndpointCommand : Command 9 | { 10 | [Argument(0, Name = "id", Description = "The id of the endpoint being added")] 11 | public string Id { get; init; } = string.Empty; 12 | 13 | [Argument(1, Name = "uri", Description = "The Uri of the endpoint being added")] 14 | public string Uri { get; init; } = string.Empty; 15 | 16 | [Argument(2, Name = "certpath", Description = "The full path to the .crt file to use for authentication")] 17 | public FileInfo? CertPath { get; init; } 18 | 19 | [Argument(3, Name = "keypath", Description = "The full path to the .key file to use for authentication")] 20 | public FileInfo? KeyPath { get; init; } 21 | 22 | [CommandTarget] 23 | public int Run() 24 | { 25 | return DoWork("Adding endpoint...", output => 26 | { 27 | var library = EndpointLibrary.OpenLibrary(); 28 | if (library.Endpoints.ContainsKey(Id)) 29 | { 30 | throw new InvalidOperationException($"An endpoint with an id of {Id} already exists."); 31 | } 32 | 33 | if (CertPath is null || KeyPath is null) 34 | { 35 | throw new InvalidOperationException("Both cert-path and key-path must be specified."); 36 | } 37 | 38 | var endpoint = new Endpoint() 39 | { 40 | Id = Id, 41 | EndpointInfo = new EndpointInfo() 42 | { 43 | Uri = new Uri(Uri), 44 | CertPath = CertPath.FullName, 45 | KeyPath = KeyPath.FullName 46 | } 47 | }; 48 | 49 | library.Endpoints.Add(endpoint.Id, endpoint); 50 | library.Save(); 51 | 52 | output.WriteMarkupLine($"Endpoint [wheat1]{endpoint.Id}[/] added"); 53 | output.WriteOutput(endpoint); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/JoinPoolCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.PlotNft; 8 | 9 | internal sealed class JoinPoolCommand : WalletCommand 10 | { 11 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 12 | public uint Id { get; init; } = 1; 13 | 14 | [Option("u", "pool-url", Description = "HTTPS host:port of the pool to join.")] 15 | public Uri PoolUrl { get; init; } = new Uri("http://localhost"); 16 | 17 | [Option("f", "force", Description = "Do not prompt before joining")] 18 | public bool Force { get; init; } 19 | 20 | protected async override Task Confirm(ICommandOutput output) 21 | { 22 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 23 | var proxy = await Login(rpcClient, output); 24 | 25 | var msg = await proxy.ValidatePoolingOptions(true, PoolUrl, TimeoutMilliseconds); 26 | return output.Confirm(msg, Force); 27 | } 28 | 29 | [CommandTarget] 30 | public async Task Run() 31 | { 32 | return await DoWorkAsync("Joining pool...", async output => 33 | { 34 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 35 | var proxy = await Login(rpcClient, output); 36 | 37 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 38 | var wallet = new PoolWallet(Id, proxy); 39 | await wallet.Validate(cts.Token); 40 | 41 | var poolInfo = await PoolUrl.GetPoolInfo(TimeoutMilliseconds); 42 | var tx = await wallet.JoinPool(poolInfo.TargetPuzzleHash ?? string.Empty, PoolUrl.ToString(), poolInfo.RelativeLockHeight, cts.Token); 43 | 44 | if (Json) 45 | { 46 | output.WriteOutput(tx); 47 | } 48 | else 49 | { 50 | PrintTransactionSentTo(output, tx); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rchia/Connections/ListConnectionsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using rchia.Commands; 8 | 9 | namespace rchia.Connections; 10 | 11 | internal sealed class ListConnectionsCommand : EndpointOptions 12 | { 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Retrieving connections...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 19 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | var connections = await proxy.GetConnections(cts.Token); 23 | 24 | if (Json) 25 | { 26 | output.WriteOutput(connections); 27 | } 28 | else 29 | { 30 | var table = from c in await proxy.GetConnections(cts.Token) 31 | select new Dictionary() 32 | { 33 | { "Type", c.Type }, 34 | { "IP", c.PeerHost }, 35 | { "Ports", $"{c.PeerPort}/{c.PeerServerPort}" }, 36 | { "NodeID", Verbose ? c.NodeId : string.Concat(c.NodeId.AsSpan(2, 10), "...") }, 37 | { "Last Connect", $"{c.LastMessageDateTime.ToLocalTime():MMM dd HH:mm}" }, 38 | { "Up", (c.BytesRead ?? 0).ToBytesString("N1") }, 39 | { "Down", (c.BytesWritten ?? 0).ToBytesString("N1") }, 40 | { "Height", c.PeakHeight }, 41 | { "Hash", string.IsNullOrEmpty(c.PeakHash) ? "no info" : Verbose ? c.PeakHash : string.Concat(c.PeakHash.AsSpan(2, 10), "...") } 42 | }; 43 | 44 | output.WriteOutput(table); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/rchia/Plots/ShowPlotLogCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Plots; 9 | 10 | internal sealed class ShowPlotLogCommand : EndpointOptions 11 | { 12 | [Option("i", "id", Description = "The id of the plot log. Omit to see logs for all running plots.")] 13 | public string? Id { get; init; } 14 | 15 | [CommandTarget] 16 | public async Task Run() 17 | { 18 | return await DoWorkAsync("Retrieving plot log...", async output => 19 | { 20 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 21 | var proxy = new PlotterProxy(rpcClient, ClientFactory.Factory.OriginService); 22 | 23 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 24 | var q = await proxy.RegisterPlotter(cts.Token); 25 | 26 | if (string.IsNullOrEmpty(Id)) 27 | { 28 | if (Json) 29 | { 30 | output.WriteOutput(q); 31 | } 32 | else 33 | { 34 | var running = q.Where(p => p.PlotState == PlotState.RUNNING); 35 | var count = running.Count(); 36 | output.WriteMarkupLine($"There {(count == 1 ? "is" : "are")} [wheat1]{count}[/] running plot job{(count == 1 ? "" : "s")}"); 37 | foreach (var plot in running) 38 | { 39 | output.WriteMarkupLine($"Log for plot [wheat1]{plot.Id}[/]"); 40 | output.WriteLine(plot.Log); 41 | output.WriteLine(""); 42 | } 43 | } 44 | } 45 | else 46 | { 47 | var plot = q.FirstOrDefault(p => p.Id == Id); 48 | if (plot is null) 49 | { 50 | throw new InvalidOperationException($"No plot with an id of {Id} was found"); 51 | } 52 | 53 | output.WriteOutput("log", plot.Log ?? plot.State, Verbose); 54 | } 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/rchia/EndpointLibrary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Newtonsoft.Json; 6 | 7 | namespace rchia; 8 | 9 | public class EndpointLibrary 10 | { 11 | internal static EndpointLibrary OpenLibrary() 12 | { 13 | var config = Settings.GetSettings(); 14 | var library = new EndpointLibrary(config.endpointfile ?? Settings.DefaultEndpointsFilePath); 15 | library.Open(); 16 | 17 | return library; 18 | } 19 | 20 | private readonly string _endpointsFilePath; 21 | 22 | public EndpointLibrary(string endpointsFilePath) 23 | { 24 | _endpointsFilePath = endpointsFilePath; 25 | } 26 | 27 | public Endpoint GetDefault() 28 | { 29 | var endpoint = Endpoints.FirstOrDefault(kvp => kvp.Value.IsDefault); 30 | 31 | return endpoint.Value ?? throw new InvalidOperationException("No default endpoint is set. Try './rchia endpoints set-default ID'"); 32 | } 33 | 34 | public IDictionary Endpoints { get; private set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); 35 | 36 | public void Open() 37 | { 38 | if (File.Exists(_endpointsFilePath)) 39 | { 40 | try 41 | { 42 | using var reader = new StreamReader(_endpointsFilePath); 43 | var json = reader.ReadToEnd(); 44 | var library = JsonConvert.DeserializeObject>(json) ?? new Dictionary(); 45 | 46 | Endpoints = new Dictionary(library, StringComparer.OrdinalIgnoreCase); 47 | } 48 | catch (Exception e) 49 | { 50 | throw new Exception($"The file {_endpointsFilePath} might be corrupt.", e); 51 | } 52 | } 53 | } 54 | 55 | public void Save() 56 | { 57 | if (Endpoints.Count == 1) 58 | { 59 | Endpoints.Values.First().IsDefault = true; 60 | } 61 | 62 | var json = JsonConvert.SerializeObject(Endpoints, Formatting.Indented); 63 | using var writer = new StreamWriter(_endpointsFilePath); 64 | writer.WriteLine(json); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/rchia/Commands/JsonOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Spectre.Console; 5 | 6 | namespace rchia.Commands; 7 | 8 | internal class JsonOutput : ICommandOutput 9 | { 10 | public bool Verbose { get; init; } 11 | 12 | public IStatus Status => new ConsoleStatus(); 13 | 14 | public ICommandOutput SetContext(StatusContext? context) 15 | { 16 | return this; 17 | } 18 | 19 | public void WriteOutput(IDictionary>> output) 20 | { 21 | AnsiConsole.WriteLine(output.ToJson()); 22 | } 23 | 24 | public void WriteOutput(string name, object? value, bool verbose = false) 25 | { 26 | var output = new Dictionary() 27 | { 28 | { name, value } 29 | }; 30 | 31 | WriteOutput(output); 32 | } 33 | 34 | public void WriteOutput(object output) 35 | { 36 | AnsiConsole.WriteLine(output.ToJson()); 37 | } 38 | 39 | public void WriteOutput(IEnumerable> output) 40 | { 41 | AnsiConsole.WriteLine(output.SortAll().ToJson()); 42 | } 43 | 44 | public void WriteOutput(IEnumerable output) 45 | { 46 | AnsiConsole.WriteLine(output.ToJson()); 47 | } 48 | 49 | public void WriteOutput(IDictionary output) 50 | { 51 | AnsiConsole.WriteLine(output.Sort().ToJson()); 52 | } 53 | 54 | public void WriteMarkupLine(string msg) 55 | { 56 | Debug.WriteLine(msg); 57 | } 58 | 59 | public void WriteLine(string msg) 60 | { 61 | Debug.WriteLine(msg); 62 | } 63 | 64 | public void WriteMessage(string msg, bool important = false) 65 | { 66 | Debug.WriteLine(msg); 67 | } 68 | 69 | public void WriteWarning(string msg) 70 | { 71 | Debug.WriteLine(msg); 72 | } 73 | 74 | public void WriteError(Exception e) 75 | { 76 | WriteOutput("error", e.Message); 77 | } 78 | 79 | public bool Confirm(string warning, bool force) 80 | { 81 | if (!force) 82 | { 83 | throw new InvalidOperationException($"This operation requires confirmation and the force flag is not set.\n\t{warning}\nAborting."); 84 | } 85 | 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/rchia/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Dynamic; 4 | using System.IO; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | 8 | namespace rchia; 9 | 10 | internal static class Settings 11 | { 12 | static Settings() 13 | { 14 | Initialize(); 15 | } 16 | 17 | public static string SettingsDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".rchia"); 18 | 19 | public static string SettingsFilePath => Path.Combine(SettingsDirectory, "settings.json"); 20 | 21 | public static string DefaultEndpointsFilePath => Path.Combine(SettingsDirectory, "endpoints.json"); 22 | 23 | public static dynamic GetSettings() 24 | { 25 | try 26 | { 27 | using var reader = new StreamReader(SettingsFilePath); 28 | var json = reader.ReadToEnd(); 29 | var settings = JsonConvert.DeserializeObject(json, new ExpandoObjectConverter()); 30 | 31 | return settings ?? GetDefaultSettings(); 32 | } 33 | catch (Exception e) 34 | { 35 | Debug.WriteLine(e.Message); 36 | 37 | return GetDefaultSettings(); 38 | } 39 | } 40 | 41 | private static dynamic GetDefaultSettings() 42 | { 43 | dynamic settings = new ExpandoObject(); 44 | settings.endpointsfile = DefaultEndpointsFilePath; 45 | return settings; 46 | } 47 | 48 | private static void Initialize() 49 | { 50 | try 51 | { 52 | // create the settings folder and file if they don't exist 53 | if (!Directory.Exists(SettingsDirectory)) 54 | { 55 | _ = Directory.CreateDirectory(SettingsDirectory); 56 | } 57 | 58 | if (!File.Exists(SettingsFilePath)) 59 | { 60 | var defaultEndPointFile = Path.Combine(SettingsDirectory, SettingsFilePath); 61 | 62 | var json = JsonConvert.SerializeObject(GetDefaultSettings(), Formatting.Indented, new ExpandoObjectConverter()); 63 | 64 | using var writer = new StreamWriter(SettingsFilePath, false); 65 | writer.WriteLine(json); 66 | } 67 | } 68 | catch (Exception e) 69 | { 70 | Debug.WriteLine(e.Message); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/rchia/Wallets/ListTransactionsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Wallet; 9 | 10 | internal sealed class ListTransactionsCommand : WalletCommand 11 | { 12 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 13 | public uint Id { get; init; } = 1; 14 | 15 | [Option("s", "start", Default = 0, Description = "The start index of transactions to show")] 16 | public uint Start { get; init; } 17 | 18 | [Option("c", "count", Description = "The max number of transactions to show. If not specified, all transactions will be shown")] 19 | public uint? Count { get; init; } 20 | 21 | [CommandTarget] 22 | public async Task Run() 23 | { 24 | return await DoWorkAsync("Retrieving transactions...", async output => 25 | { 26 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 27 | var wallet = new chia.dotnet.Wallet(Id, await Login(rpcClient, output)); 28 | 29 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 30 | var (NetworkName, NetworkPrefix) = await wallet.WalletProxy.GetNetworkInfo(cts.Token); 31 | 32 | var count = Count is null ? await wallet.GetTransactionCount(cts.Token) : Count.Value; 33 | var transactions = await wallet.GetTransactions(Start, count - Start, cts.Token); 34 | 35 | if (transactions.Any()) 36 | { 37 | var result = new List>(); 38 | 39 | foreach (var tx in transactions) 40 | { 41 | var dict = new Dictionary(); 42 | FormatTransaction(tx, NetworkPrefix, dict); 43 | result.Add(dict); 44 | } 45 | 46 | output.WriteOutput(result); 47 | var c = transactions.Count(); 48 | output.WriteMarkupLine($"Showing [wheat1]{c}[/] transaction{(c == 1 ? string.Empty : "s")}"); 49 | } 50 | else 51 | { 52 | output.WriteOutput("warning", "There are no transactions to this address", false); 53 | } 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rchia/rchia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | true 8 | dkackman 9 | 0.9.1 10 | A cross platform (Linux, Windows, MacOS) command line utility that mirrors the chia CLI, but uses RPC rather than running locally on the node. This allows management of any number of nodes from a central location as long as their RPC interface is exposed on the network. 11 | 2021 Don Kackman 12 | LICENSE 13 | https://github.com/dkackman/rchia 14 | chia-leaf-logo-384x384.png 15 | 16 | https://github.com/dkackman/rchia 17 | git 18 | chia chia-dotnet 19 | en 20 | 0.9.1 21 | - bug fix release 22 | chia.ico 23 | 0.9.1.0 24 | 0.9.1.0 25 | 26 | 27 | 28 | 1701;1702;NU5104 29 | 30 | 31 | 32 | 1701;1702;NU5104 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | True 46 | 47 | 48 | 49 | True 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/rchia/Connections/PruneConnectionsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using rchia.Commands; 8 | 9 | namespace rchia.Connections; 10 | 11 | internal sealed class PruneConnectionsCommand : EndpointOptions 12 | { 13 | [Argument(0, Name = "blocks", Default = 10, Description = "Prune nodes that are this many blocks behind the sync tip height")] 14 | public ulong Blocks { get; init; } = 10; 15 | 16 | [CommandTarget] 17 | public async Task Run() 18 | { 19 | return await DoWorkAsync("Pruning connections...", async output => 20 | { 21 | if (Blocks < 1) 22 | { 23 | throw new InvalidOperationException("A number of blocks must be provided"); 24 | } 25 | 26 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 27 | var fullNode = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 28 | 29 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 30 | var state = await fullNode.GetBlockchainState(cts.Token); 31 | if (state.Peak is null) 32 | { 33 | throw new InvalidOperationException("Node has no peak. Aborting."); 34 | } 35 | 36 | var maxHeight = state.Peak.Height - Blocks; 37 | output.WriteMarkupLine($"Pruning connections that with a peak less than [wheat1]{maxHeight}[/]"); 38 | 39 | var connections = await fullNode.GetConnections(cts.Token); 40 | // only prune other full nodes, not famers, harvesters, and wallets etc 41 | var n = 0; 42 | var list = new List(); 43 | foreach (var connection in connections.Where(c => c.Type == NodeType.FULL_NODE && c.PeakHeight < maxHeight)) 44 | { 45 | using var cts1 = new CancellationTokenSource(TimeoutMilliseconds); 46 | 47 | await fullNode.CloseConnection(connection.NodeId, cts1.Token); 48 | list.Add($"{connection.PeerHost}:{connection.PeerServerPort}"); 49 | n++; 50 | } 51 | output.WriteOutput(list); 52 | output.WriteMarkupLine($"Pruned [wheat1]{n}[/] connection{(n == 1 ? string.Empty : "s")}"); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/rchia/Plots/ListPlotsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Plots; 9 | 10 | internal sealed class ListPlotsCommand : EndpointOptions 11 | { 12 | [CommandTarget] 13 | public async Task Run() 14 | { 15 | return await DoWorkAsync("Retrieving plot list...", async output => 16 | { 17 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Harvester); 18 | var proxy = new HarvesterProxy(rpcClient, ClientFactory.Factory.OriginService); 19 | 20 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 21 | var plots = await proxy.GetPlots(cts.Token); 22 | 23 | var result = new List>>() 24 | { 25 | new Dictionary>() 26 | { 27 | { "failed_to_open", plots.FailedToOpenFilenames } 28 | }, 29 | new Dictionary>() 30 | { 31 | { "not_found", plots.NotFoundFileNames } 32 | }, 33 | new Dictionary>() 34 | { 35 | { "plots", plots.Plots.Select(p => p.Filename) } 36 | } 37 | }; 38 | 39 | if (!Json) 40 | { 41 | ListPlots(output, plots.FailedToOpenFilenames, "failed to open"); 42 | ListPlots(output, plots.NotFoundFileNames, "not found"); 43 | ListPlots(output, plots.Plots.Select(p => p.Filename), "plots"); 44 | if (!Verbose) 45 | { 46 | output.WriteMessage("(use '[grey]--verbose[/]' to see file names)", true); 47 | } 48 | } 49 | else 50 | { 51 | output.WriteOutput(result); 52 | } 53 | }); 54 | } 55 | 56 | private void ListPlots(ICommandOutput output, IEnumerable plots, string msg) 57 | { 58 | output.WriteLine($"{plots.Count()} {msg}."); 59 | if (plots.Any() && Verbose) 60 | { 61 | foreach (var plot in plots) 62 | { 63 | output.WriteLine(plot); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/rchia/PlotNft/CreatePlotNftCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.PlotNft; 8 | 9 | public enum InitialPoolingState 10 | { 11 | pool, 12 | local 13 | } 14 | 15 | internal sealed class CreatePlotNftCommand : WalletCommand 16 | { 17 | [Option("u", "pool-url", Description = "HTTPS host:port of the pool to join. Omit for self pooling")] 18 | public Uri? PoolUrl { get; init; } 19 | 20 | [Option("s", "state", IsRequired = true, Description = "Initial state of Plot NFT")] 21 | public InitialPoolingState State { get; init; } 22 | 23 | [Option("f", "force", Description = "Do not prompt before nft creation")] 24 | public bool Force { get; init; } 25 | 26 | protected async override Task Confirm(ICommandOutput output) 27 | { 28 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 29 | var proxy = await Login(rpcClient, output); 30 | 31 | var msg = await proxy.ValidatePoolingOptions(State == InitialPoolingState.pool, PoolUrl, TimeoutMilliseconds); 32 | 33 | return output.Confirm(msg, Force); 34 | } 35 | 36 | [CommandTarget] 37 | public async Task Run() 38 | { 39 | return await DoWorkAsync("Creating pool NFT and wallet...", async output => 40 | { 41 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 42 | var proxy = await Login(rpcClient, output); 43 | 44 | var poolInfo = PoolUrl is not null ? await PoolUrl.GetPoolInfo(TimeoutMilliseconds) : new PoolInfo(); 45 | var poolState = new PoolState() 46 | { 47 | PoolUrl = PoolUrl?.ToString(), 48 | State = State == InitialPoolingState.pool ? PoolSingletonState.FARMING_TO_POOL : PoolSingletonState.SELF_POOLING, 49 | TargetPuzzleHash = poolInfo.TargetPuzzleHash!, 50 | RelativeLockHeight = poolInfo.RelativeLockHeight 51 | }; 52 | 53 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 54 | var result = await proxy.CreatePoolWallet(poolState, null, null, cts.Token); 55 | 56 | if (Json) 57 | { 58 | output.WriteOutput(result); 59 | } 60 | else 61 | { 62 | output.WriteOutput("launcher_id", result.launcherId, Verbose); 63 | 64 | PrintTransactionSentTo(output, result.transaction); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/rchia/WalletCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using chia.dotnet.bech32; 8 | using rchia.Commands; 9 | 10 | namespace rchia; 11 | 12 | internal abstract class WalletCommand : EndpointOptions 13 | { 14 | [Option("fp", "fingerprint", Description = "Set the fingerprint to specify which wallet to use - the first fingerprint will be used if not set")] 15 | public uint? Fingerprint { get; init; } 16 | 17 | protected void PrintTransactionSentTo(ICommandOutput output, TransactionRecord tx) 18 | { 19 | var result = new Dictionary() 20 | { 21 | { "transaction", tx.Name }, 22 | { "sent_to", tx.SentTo.Select(peer => peer.Peer) } 23 | }; 24 | output.WriteOutput(result); 25 | output.WriteMessage($"Do '[grey]rchia wallet get-transaction -tx {tx.Name}[/]' to get status"); 26 | } 27 | 28 | protected void FormatTransaction(TransactionRecord tx, string prefix, IDictionary row) 29 | { 30 | var name = Verbose || Json ? tx.Name : string.Concat(tx.Name.AsSpan(2, 10), "..."); 31 | var status = tx.Confirmed 32 | ? new Formattable("Confirmed", "green") 33 | : tx.IsInMempool 34 | ? new Formattable("In mempool", "orange3") 35 | : new Formattable("Pending", "grey"); 36 | 37 | var amount = tx.Amount.ToChia(); 38 | var bech32 = new Bech32M(prefix); 39 | var to = Verbose || Json ? bech32.PuzzleHashToAddress(tx.ToPuzzleHash) : string.Concat(bech32.PuzzleHashToAddress(tx.ToPuzzleHash).AsSpan(prefix.Length, 10), "..."); 40 | var at = tx.CreatedAtDateTime.ToLocalTime(); 41 | 42 | row.Add("name", name); 43 | row.Add("status", status); 44 | row.Add("amount", amount); 45 | row.Add("to", to); 46 | row.Add("at", at); 47 | } 48 | 49 | protected async Task Login(IRpcClient rpcClient, ICommandOutput output) 50 | { 51 | using var message = new StatusMessage(output.Status, "Logging into wallet..."); 52 | var walletProxy = new WalletProxy(rpcClient, ClientFactory.Factory.OriginService); 53 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 54 | 55 | if (Fingerprint.HasValue) 56 | { 57 | _ = await walletProxy.LogIn(Fingerprint.Value, false, cts.Token); 58 | } 59 | else 60 | { 61 | _ = await walletProxy.LogIn(false, cts.Token); 62 | } 63 | 64 | return walletProxy; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/rchia/Services/StartServicesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Services; 9 | 10 | internal sealed class StartServicesCommand : EndpointOptions 11 | { 12 | [Argument(0, Name = "service-group", Description = "[all|node|harvester|farmer|farmer-no-wallet|farmer-only|timelord|\ntimelord-only|timelord-launcher-only|wallet|wallet-only|introducer|simulator]")] 13 | public string? ServiceGroup { get; init; } 14 | 15 | [Option("r", "restart", Description = "Restart the specified service(s)")] 16 | public bool Restart { get; init; } 17 | 18 | [CommandTarget] 19 | public async Task Run() 20 | { 21 | return await DoWorkAsync($"{(Restart ? "Res" : "S")}tarting services...", async output => 22 | { 23 | if (ServiceGroup is null || !ServiceGroups.Groups.ContainsKey(ServiceGroup)) 24 | { 25 | throw new InvalidOperationException($"Unrecognized service group {ServiceGroup}. It must be one of\n {string.Join('|', ServiceGroups.Groups.Keys)}."); 26 | } 27 | 28 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 29 | 30 | var proxy = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 31 | 32 | var result = new List(); 33 | foreach (var service in ServiceGroups.Groups[ServiceGroup]) 34 | { 35 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 36 | var isRunnnig = await proxy.IsServiceRunning(service, cts.Token); 37 | 38 | if (isRunnnig && !Restart) 39 | { 40 | output.WriteMarkupLine($"[wheat1]{service}[/] is already running. Use -r to restart it..."); 41 | } 42 | else 43 | { 44 | if (isRunnnig && Restart) 45 | { 46 | output.WriteMarkupLine($"Stopping [wheat1]{service}[/]..."); 47 | using var cts2 = new CancellationTokenSource(TimeoutMilliseconds); 48 | await proxy.StopService(service, cts2.Token); 49 | } 50 | 51 | output.WriteMarkupLine($"Starting [wheat1]{service}[/]..."); 52 | using var cts3 = new CancellationTokenSource(TimeoutMilliseconds); 53 | await proxy.StartService(service, cts3.Token); 54 | } 55 | 56 | result.Add(service); 57 | } 58 | 59 | output.WriteOutput(result); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/rchia/Wallets/SendTransactionCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Wallet; 9 | 10 | internal sealed class SendTransactionCommand : WalletCommand 11 | { 12 | [Option("a", "amount", IsRequired = true, Description = "How much chia to send, in XCH")] 13 | public decimal Amount { get; init; } 14 | 15 | [Option("m", "fee", Default = 0, Description = "Set the fees for the transaction, in XCH")] 16 | public decimal Fee { get; init; } 17 | 18 | [Option("t", "address", IsRequired = true, Description = "Address to send the XCH")] 19 | public string Address { get; init; } = string.Empty; 20 | 21 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 22 | public uint Id { get; init; } = 1; 23 | 24 | [Option("f", "force", Description = "If Fee > Amount, send the transaction anyway")] 25 | public bool Force { get; init; } 26 | 27 | [CommandTarget] 28 | public async Task Run() 29 | { 30 | return await DoWorkAsync("Sending Transaction...", async output => 31 | { 32 | if (string.IsNullOrEmpty(Address)) 33 | { 34 | throw new InvalidOperationException("Address cannot be empty"); 35 | } 36 | 37 | if (Amount < 0) 38 | { 39 | throw new InvalidOperationException("Amount cannot be negative"); 40 | } 41 | 42 | if (Fee < 0) 43 | { 44 | throw new InvalidOperationException("Fee cannot be negative"); 45 | } 46 | 47 | if (Fee > Amount && !Force) 48 | { 49 | output.WriteWarning($"A transaction of amount {Amount} and fee {Fee} is unusual."); 50 | throw new InvalidOperationException("Pass in --force if you are sure you mean to do this."); 51 | } 52 | 53 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 54 | var wallet = new chia.dotnet.Wallet(Id, await Login(rpcClient, output)); 55 | 56 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 57 | var (_, NetworkPrefix) = await wallet.WalletProxy.GetNetworkInfo(cts.Token); 58 | var tx = await wallet.SendTransaction(Address, Amount.ToMojo(), Fee.ToMojo(), cts.Token); 59 | 60 | var result = new Dictionary(); 61 | FormatTransaction(tx, NetworkPrefix, result); 62 | output.WriteOutput(result); 63 | 64 | output.WriteMessage($"Do '[grey]rchia wallet get-transaction -tx {tx.TransactionId}[/]' to get status"); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/rchia/Node/NetspaceCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Node; 9 | 10 | [Command("netspace", Description = "Calculates the estimated space on the network given two block header hashes.\nRequires a daemon or full_node endpoint.")] 11 | internal sealed class NetspaceCommand : EndpointOptions 12 | { 13 | [Option("d", "delta-block-height", Default = 4608, ArgumentHelpName = "DELTA", Description = "Compare a block X blocks older to estimate total network space. Defaults to 4608 blocks" + 14 | "(~1 day) and Peak block as the starting block. Use --start HEADER_HASH to specify" + 15 | "starting block. Use 192 blocks to estimate over the last hour.")] 16 | public uint DeltaBlockHeight { get; init; } = 4608; 17 | 18 | [Option("s", "start", ArgumentHelpName = "HEADER_HASH", Description = "Newest block used to calculate estimated total network space.Defaults to Peak block.")] 19 | public string? Start { get; init; } 20 | 21 | [CommandTarget] 22 | public async Task Run() 23 | { 24 | return await DoWorkAsync("Retrieving network info...", async output => 25 | { 26 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 27 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 28 | 29 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 30 | uint newer_block_height = 0; 31 | if (string.IsNullOrEmpty(Start)) 32 | { 33 | var blockchain_state = await proxy.GetBlockchainState(cts.Token); 34 | if (blockchain_state.Peak is null) 35 | { 36 | output.WriteWarning("No blocks in blockchain"); 37 | return; 38 | } 39 | 40 | newer_block_height = blockchain_state.Peak.Height; 41 | } 42 | else 43 | { 44 | var newer_block = await proxy.GetBlockRecord(Start, cts.Token); 45 | if (newer_block is null) 46 | { 47 | output.WriteWarning($"Block header hash {Start} not found."); 48 | return; 49 | } 50 | 51 | newer_block_height = newer_block.Height; 52 | } 53 | 54 | var newer_block_header = await proxy.GetBlockRecordByHeight(newer_block_height); 55 | var older_block_height = Math.Max(0, newer_block_height - DeltaBlockHeight); 56 | var older_block_header = await proxy.GetBlockRecordByHeight(older_block_height); 57 | var network_space_estimate = await proxy.GetNetworkSpace(newer_block_header.HeaderHash, older_block_header.HeaderHash); 58 | 59 | output.WriteOutput("network_space_estimate", new Formattable(network_space_estimate, space => space.ToBytesString()), Verbose); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/rchia/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Spectre.Console; 5 | 6 | namespace rchia.Commands; 7 | 8 | public abstract class Command 9 | { 10 | [Option("v", "verbose", Description = "Set output to verbose messages")] 11 | public bool Verbose { get; init; } 12 | 13 | [Option("", "json", Description = "Set this flag to output json", IsHidden = true)] 14 | public bool Json { get; init; } 15 | 16 | protected async virtual Task Confirm(ICommandOutput output) { await Task.CompletedTask; return true; } 17 | 18 | protected async Task DoWorkAsync(string msg, Func work) 19 | { 20 | Debug.Assert(!string.IsNullOrEmpty(msg)); 21 | var output = CreateCommandOutput(); 22 | 23 | try 24 | { 25 | if (await Confirm(output)) 26 | { 27 | if (Json) 28 | { 29 | await work(output); 30 | } 31 | else 32 | { 33 | await AnsiConsole.Status() 34 | .AutoRefresh(true) 35 | .SpinnerStyle(Style.Parse("green bold")) 36 | .StartAsync(msg, async ctx => await work(output.SetContext(ctx))); 37 | 38 | output.WriteMessage("Done."); 39 | } 40 | } 41 | return 0; 42 | } 43 | catch (TaskCanceledException) 44 | { 45 | output.WriteMarkupLine($"[red]The operation timed out[/]"); 46 | output.WriteMessage("Check that the chia service is running and available. You can extend the timeout period by using the '-to' option.", true); 47 | 48 | return -1; 49 | } 50 | catch (Exception e) 51 | { 52 | output.WriteError(e); 53 | 54 | return -1; 55 | } 56 | } 57 | 58 | protected int DoWork(string msg, Action work) 59 | { 60 | var output = CreateCommandOutput(); 61 | 62 | try 63 | { 64 | // when outputing json there are no status or progress indicators 65 | if (Json) 66 | { 67 | work(output); 68 | } 69 | else 70 | { 71 | AnsiConsole.Status() 72 | .AutoRefresh(true) 73 | .SpinnerStyle(Style.Parse("green bold")) 74 | .Start(msg, ctx => work(output.SetContext(ctx))); 75 | 76 | output.WriteMessage("Done."); 77 | } 78 | 79 | return 0; 80 | } 81 | catch (Exception e) 82 | { 83 | output.WriteError(e); 84 | 85 | return -1; 86 | } 87 | } 88 | 89 | private ICommandOutput CreateCommandOutput() 90 | { 91 | if (Json) 92 | { 93 | return new JsonOutput() 94 | { 95 | Verbose = Verbose 96 | }; 97 | } 98 | 99 | return new ConsoleOutput() 100 | { 101 | Verbose = Verbose 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/rchia/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using chia.dotnet; 8 | using Newtonsoft.Json; 9 | 10 | namespace rchia; 11 | 12 | internal static class Extensions 13 | { 14 | public static IDictionary Sort(this IDictionary dict) where K : notnull 15 | { 16 | return dict.OrderBy(kvp => kvp.Key).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); 17 | } 18 | 19 | public static IEnumerable> SortAll(this IEnumerable> dict) where K : notnull 20 | { 21 | return dict.Select(d => d.Sort()); 22 | } 23 | 24 | public static string FromSnakeCase(this string s) 25 | { 26 | return string.IsNullOrEmpty(s) 27 | ? string.Empty 28 | : new StringBuilder(s.Length) 29 | .Append(char.ToUpper(s[0])) 30 | .Append(s.Replace('_', ' ')[1..]) 31 | .ToString(); 32 | } 33 | 34 | public static string ToJson(this object o) 35 | { 36 | return JsonConvert.SerializeObject(o, Formatting.Indented); 37 | } 38 | 39 | public static string FormatTimeSpan(this TimeSpan t) 40 | { 41 | var builder = new StringBuilder(); 42 | if (t.Days > 0) 43 | { 44 | _ = builder.Append($"{t.Days} day{(t.Days != 1 ? "s" : "")} "); 45 | } 46 | 47 | if (t.Hours > 0) 48 | { 49 | _ = builder.Append($"{t.Hours} hour{(t.Hours != 1 ? "s" : "")} "); 50 | } 51 | 52 | if (t.Minutes > 0) 53 | { 54 | _ = builder.Append($"{t.Minutes} minute{(t.Minutes != 1 ? "s" : "")} "); 55 | } 56 | 57 | return builder.ToString(); 58 | } 59 | 60 | public async static Task ValidatePoolingOptions(this WalletProxy wallet, bool pooling, Uri? poolUri, int timeoutMilliseconds) 61 | { 62 | using var cts = new CancellationTokenSource(timeoutMilliseconds); 63 | var (NetworkName, NetworkPrefix) = await wallet.GetNetworkInfo(cts.Token); 64 | 65 | if (pooling && NetworkName == "mainnet" && poolUri is not null && poolUri.Scheme != "https") 66 | { 67 | throw new InvalidOperationException($"Pool URLs must be HTTPS on mainnet {poolUri}. Aborting."); 68 | } 69 | 70 | return $"This operation will join the wallet with fingerprint [wheat1]{wallet.Fingerprint}[/] to [wheat1]{poolUri}[/].\nDo you want to proceed?"; 71 | } 72 | 73 | public async static Task GetPoolInfo(this Uri uri, int timeoutMilliseconds) 74 | { 75 | using var cts = new CancellationTokenSource(timeoutMilliseconds); 76 | var info = await WalletProxy.GetPoolInfo(uri, cts.Token); 77 | 78 | if (info.RelativeLockHeight > 1000) 79 | { 80 | throw new InvalidOperationException("Relative lock height too high for this pool, cannot join"); 81 | } 82 | 83 | if (info.ProtocolVersion != PoolInfo.POOL_PROTOCOL_VERSION) 84 | { 85 | throw new InvalidOperationException($"Unsupported version: {info.ProtocolVersion}, should be {PoolInfo.POOL_PROTOCOL_VERSION}"); 86 | } 87 | 88 | return info; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/rchia/Services/StopServicesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Services 9 | { 10 | internal sealed class StopServicesCommand : EndpointOptions 11 | { 12 | [Argument(0, Name = "service-group", Description = "[all|node|harvester|farmer|farmer-no-wallet|farmer-only|timelord|\ntimelord-only|timelord-launcher-only|wallet|wallet-only|introducer|simulator]")] 13 | public string? ServiceGroup { get; init; } 14 | 15 | [Option("d", "daemon", Description = "Stop the daemon service as well\nThe daemon cannot be restarted remotely")] 16 | public bool Daemon { get; init; } 17 | 18 | [Option("f", "force", Description = "If specified in conjunction with '-d', shut down the daemon without prompting")] 19 | public bool Force { get; init; } 20 | 21 | protected async override Task Confirm(ICommandOutput output) 22 | { 23 | await Task.CompletedTask; 24 | 25 | if (Daemon && !output.Confirm("The daemon cannot be restared remotely. You will need shell access to the node in order to restart it.\nAre you sure you want to stop the daemon?", Force)) 26 | { 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | [CommandTarget] 34 | public async Task Run() 35 | { 36 | return await DoWorkAsync("Stopping services...", async output => 37 | { 38 | if (ServiceGroup is null || !ServiceGroups.Groups.ContainsKey(ServiceGroup)) 39 | { 40 | throw new InvalidOperationException($"Unrecognized service group {ServiceGroup}. It must be one of\n {string.Join("|", ServiceGroups.Groups.Keys)}."); 41 | } 42 | 43 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 44 | var proxy = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 45 | 46 | var result = new List(); 47 | foreach (var service in ServiceGroups.Groups[ServiceGroup]) 48 | { 49 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 50 | var isRunnnig = await proxy.IsServiceRunning(service, cts.Token); 51 | 52 | if (isRunnnig) 53 | { 54 | output.WriteMarkupLine($"Stopping [wheat1]{service}[/]..."); 55 | await proxy.StopService(service, cts.Token); 56 | } 57 | else 58 | { 59 | output.WriteMarkupLine($"[wheat1]{service}[/] is not running..."); 60 | } 61 | 62 | result.Add(service); 63 | } 64 | 65 | if (Daemon) 66 | { 67 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 68 | await proxy.Exit(cts.Token); 69 | result.Add("daemon"); 70 | } 71 | 72 | output.WriteOutput(result); 73 | }); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/rchia/Blocks/AdditionsAndRemovalsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | 8 | namespace rchia.Blocks; 9 | 10 | internal sealed class AdditionsAndRemovalsCommand : EndpointOptions 11 | { 12 | [Option("a", "hash", ArgumentHelpName = "HASH", Description = "Look up by header hash")] 13 | public string? Hash { get; init; } 14 | 15 | [Option("h", "height", ArgumentHelpName = "HEIGHT", Description = "Look up by height")] 16 | public uint? Height { get; init; } 17 | 18 | private async Task GetHash(FullNodeProxy proxy) 19 | { 20 | if (Height.HasValue) 21 | { 22 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 23 | var block = await proxy.GetBlockRecordByHeight(Height.Value, cts.Token); 24 | 25 | return block.HeaderHash; 26 | } 27 | 28 | if (!string.IsNullOrEmpty(Hash)) 29 | { 30 | return Hash; 31 | } 32 | 33 | throw new InvalidOperationException("Either a valid block height or header hash must be specified."); 34 | } 35 | 36 | [CommandTarget] 37 | public async Task Run() 38 | { 39 | return await DoWorkAsync("Retrieving block header...", async output => 40 | { 41 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 42 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 43 | var headerHash = await GetHash(proxy); 44 | 45 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 46 | var (Additions, Removals) = await proxy.GetAdditionsAndRemovals(headerHash, cts.Token); 47 | 48 | if (Json) 49 | { 50 | var result = new Dictionary> 51 | { 52 | { "additions", Additions }, 53 | { "removals", Removals } 54 | }; 55 | output.WriteOutput(result); 56 | } 57 | else 58 | { 59 | var result = new Dictionary>> 60 | { 61 | { "additions", GetCoinRecords(Additions) }, 62 | { "removals", GetCoinRecords(Removals) } 63 | }; 64 | output.WriteOutput(result); 65 | } 66 | }); 67 | } 68 | 69 | private static IEnumerable> GetCoinRecords(IEnumerable records) 70 | { 71 | var table = new List>(); 72 | 73 | foreach (var record in records) 74 | { 75 | var row = new Dictionary 76 | { 77 | { "parent_coin_info", record.Coin.ParentCoinInfo.Replace("0x", "") }, 78 | { "puzzle_hash", record.Coin.PuzzleHash.Replace("0x", "") }, 79 | { "amount", record.Coin.Amount.ToChia() }, 80 | { "confirmed_at", record.ConfirmedBlockIndex }, 81 | { "spent_at", record.SpentBlockIndex }, 82 | { "spent", record.Spent }, 83 | { "coinbase", record.Coinbase }, 84 | { "timestamp", record.DateTimestamp.ToLocalTime() } 85 | }; 86 | 87 | table.Add(row); 88 | } 89 | 90 | return table; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/rchia/Wallets/ShowWalletCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using rchia.Commands; 8 | 9 | namespace rchia.Wallet; 10 | 11 | internal sealed class ShowWalletCommand : WalletCommand 12 | { 13 | [CommandTarget] 14 | public async Task Run() 15 | { 16 | return await DoWorkAsync("Retrieving wallet info...", async output => 17 | { 18 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.Wallet); 19 | var proxy = await Login(rpcClient, output); 20 | 21 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 22 | var (GenesisInitialized, Synced, Syncing) = await proxy.GetSyncStatus(cts.Token); 23 | var (NetworkName, NetworkPrefix) = await proxy.GetNetworkInfo(cts.Token); 24 | var height = await proxy.GetHeightInfo(cts.Token); 25 | var wallets = await proxy.GetWallets(cts.Token); 26 | 27 | var syncStatus = Synced ? 28 | new Formattable("Synced", "green") : 29 | Syncing ? 30 | new Formattable("Syncing", "orange3") : 31 | new Formattable("Not synced", "red"); 32 | 33 | var wallet = new Dictionary() 34 | { 35 | // the successful call to Login above means we have a fingerprint for the wallet 36 | { "fingerprint", new Formattable(proxy.Fingerprint!.Value, fp => $"{fp}") }, 37 | { "sync_status", syncStatus }, 38 | { "wallet_height", height } 39 | }; 40 | var balances = new List>(); 41 | 42 | if (wallets.Any()) 43 | { 44 | using var status = new StatusMessage(output.Status, "Retrieving balances..."); 45 | 46 | foreach (var summary in wallets) 47 | { 48 | var newWallet = new chia.dotnet.Wallet(summary.Id, proxy); 49 | var (ConfirmedWalletBalance, UnconfirmedWalletBalance, SpendableBalance, PendingChange, MaxSendAmount, UnspentCoinCount, PendingCoinRemovalCount) 50 | = await newWallet.GetBalance(cts.Token); 51 | 52 | var row = new Dictionary 53 | { 54 | { "Id", summary.Id }, 55 | { "Name", summary.Name }, 56 | { "Type", summary.Type.ToString() }, 57 | { "Total", ConfirmedWalletBalance.ToChia() }, 58 | { "Pending Total", UnconfirmedWalletBalance.ToChia() }, 59 | { "Spendable", SpendableBalance.ToChia() } 60 | }; 61 | 62 | if (Verbose || Json) 63 | { 64 | row.Add("Pending Change", PendingChange.ToChia()); 65 | row.Add("Max Spend Amount", MaxSendAmount.ToChia()); 66 | row.Add("Unspent Coin Count", UnspentCoinCount); 67 | row.Add("Pending Coin Removal Count", PendingCoinRemovalCount); 68 | } 69 | 70 | balances.Add(row); 71 | } 72 | } 73 | else 74 | { 75 | output.WriteWarning($"There are no wallets for a this public key {proxy.Fingerprint}"); 76 | } 77 | 78 | if (Json) 79 | { 80 | dynamic result = new ExpandoObject(); 81 | result.summary = wallet; 82 | result.wallets = balances; 83 | 84 | output.WriteOutput(result); 85 | } 86 | else 87 | { 88 | output.WriteOutput(wallet); 89 | output.WriteOutput(balances); 90 | } 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/rchia/Plots/CreatePlotsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | using rchia.Commands; 6 | 7 | namespace rchia.Plots; 8 | 9 | internal sealed class CreatePlotsCommand : EndpointOptions 10 | { 11 | [Option("k", "size", Default = KValues.K32, Description = "Plot szie")] 12 | public KValues Size { get; init; } = KValues.K32; 13 | 14 | [Option("o", "override-k", Description = "Force size smaller than 32")] 15 | public bool OverrideK { get; init; } 16 | 17 | [Option("n", "num", Default = 1, Description = "Number of plots or challenges")] 18 | public int Num { get; init; } = 1; 19 | 20 | [Option("b", "buffer", Default = 3389, Description = "Megabytes for sort/plot buffer")] 21 | public int Buffer { get; init; } = 3389; 22 | 23 | [Option("r", "num-threads", Default = 2, Description = "Number of threads to use")] 24 | public int NumThreads { get; init; } = 2; 25 | 26 | [Option("u", "buckets", Default = 128, Description = "Number of buckets")] 27 | public int Buckets { get; init; } = 128; 28 | 29 | [Option("a", "alt-fingerprint", Description = "Enter the alternative fingerprint of the key you want to use")] 30 | public uint? AltFingerprint { get; init; } 31 | 32 | [Option("c", "pool-contract-address", Description = "Address of where the pool reward will be sent to. Only used if\nalt_fingerprint and pool public key are not set")] 33 | public string? PoolContractAddress { get; init; } 34 | 35 | [Option("f", "farmer-public-key", Description = "Hex farmer public key")] 36 | public string? FarmerPublicKey { get; init; } 37 | 38 | [Option("p", "pool-public-key", Description = "Hex public key of pool")] 39 | public string? PoolPublicKey { get; init; } 40 | 41 | [Option("d", "final-dir", Default = ".", Description = "Final directory for plots (relative or absolute)")] 42 | public string FinalDir { get; init; } = "."; 43 | 44 | [Option("t", "tmp-dir", Default = ".", Description = "Temporary directory for plotting files")] 45 | public string TmpDir { get; init; } = "."; 46 | 47 | [Option("2", "tmp2-dir", Description = "Second temporary directory for plotting files")] 48 | public string? Tmp2Dir { get; init; } 49 | 50 | [Option("e", "nobitfield", Description = "Disable bitfield")] 51 | public bool NoBitField { get; init; } 52 | 53 | [Option("x", "exclude-final-dir", Description = "Skips adding [final-dir] to harvester for farming")] 54 | public bool ExcludeFinalDir { get; init; } 55 | 56 | [CommandTarget] 57 | public async Task Run() 58 | { 59 | return await DoWorkAsync("Adding request to the plot queue...", async output => 60 | { 61 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 62 | var proxy = new PlotterProxy(rpcClient, ClientFactory.Factory.OriginService); 63 | 64 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 65 | _ = await proxy.RegisterPlotter(cts.Token); 66 | var ids = await proxy.StartPlotting(CreateConfig(), cts.Token); 67 | 68 | output.WriteLine($"Plot{(ids.Count() == 1 ? string.Empty : "s")} queued:"); 69 | output.WriteOutput(ids); 70 | output.WriteMessage("Run '[grey]rchia plots queue -v[/]' or '[grey]rchia plots log[/]' to check status", true); 71 | }); 72 | } 73 | 74 | private PlotterConfig CreateConfig() 75 | { 76 | return new PlotterConfig() 77 | { 78 | Size = Size, 79 | OverrideK = OverrideK, 80 | Number = Num, 81 | Buffer = Buffer, 82 | NumThreads = NumThreads, 83 | Buckets = Buckets, 84 | AltFingerprint = AltFingerprint, 85 | PoolContractAddress = PoolContractAddress, 86 | FarmerPublicKey = FarmerPublicKey, 87 | PoolPublicKey = PoolPublicKey, 88 | DestinationDir = FinalDir, 89 | TempDir = TmpDir, 90 | TempDir2 = Tmp2Dir, 91 | NoBitField = NoBitField, 92 | ExcludeFinalDir = ExcludeFinalDir 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/rchia/Node/StatusCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using rchia.Commands; 7 | using Spectre.Console; 8 | 9 | namespace rchia.Node; 10 | 11 | internal sealed class StatusCommand : EndpointOptions 12 | { 13 | [Option("a", "about", IsHidden = true)] 14 | public bool About { get; init; } 15 | 16 | [CommandTarget] 17 | public async Task Run() 18 | { 19 | return await DoWorkAsync("Retrieving node info...", async output => 20 | { 21 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 22 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 23 | 24 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 25 | var state = await proxy.GetBlockchainState(cts.Token); 26 | var (NetworkName, NetworkPrefix) = await proxy.GetNetworkInfo(cts.Token); 27 | 28 | var peakHash = state.Peak is not null ? state.Peak.HeaderHash : string.Empty; 29 | 30 | var result = new Dictionary() 31 | { 32 | { "network", NetworkName } 33 | }; 34 | 35 | if (state.Sync.Synced) 36 | { 37 | result.Add("blockchain_status", new Formattable("Full Node Synced", "green")); 38 | result.Add("peak_hash", new Formattable(peakHash, peakHash => peakHash.Replace("0x", string.Empty))); 39 | } 40 | else if (state.Peak is not null && state.Sync.SyncMode) 41 | { 42 | result.Add("blockchain_status", new Formattable("Syncing", "orange3")); 43 | result.Add("sync_height", $"{state.Sync.SyncProgressHeight:N0} of {state.Sync.SyncTipHeight:N0}"); 44 | result.Add("blocks_behind", state.Sync.SyncTipHeight - state.Sync.SyncProgressHeight); 45 | result.Add("peak_hash", new Formattable(peakHash, peakHash => peakHash.Replace("0x", string.Empty))); 46 | } 47 | else if (state.Peak is not null) 48 | { 49 | result.Add("blockchain_status", new Formattable("Not Synced", "red")); 50 | } 51 | else 52 | { 53 | output.WriteWarning("The node is searching for an initial chain"); 54 | output.WriteMarkupLine("You may be able to expedite with '[grey]rchia show add host:port[/]' using a known node."); 55 | } 56 | 57 | if (state.Peak is not null) 58 | { 59 | var peak_time = state.Peak.DateTimestamp; 60 | if (!state.Peak.IsTransactionBlock) 61 | { 62 | var curr = await proxy.GetBlockRecord(state.Peak.HeaderHash, cts.Token); 63 | 64 | while (curr is not null && !curr.IsTransactionBlock) 65 | { 66 | curr = await proxy.GetBlockRecord(curr.PrevHash, cts.Token); 67 | } 68 | 69 | peak_time = curr?.DateTimestamp; 70 | } 71 | 72 | var time = peak_time.HasValue ? peak_time.Value.ToLocalTime().ToString("F") : "unknown"; 73 | result.Add("peak_time", time); 74 | result.Add("peak_height", state.Peak.Height); 75 | } 76 | 77 | result.Add("estimated_network_space", new Formattable(state.Space, space => space.ToBytesString())); 78 | result.Add("current_difficulty", state.Difficulty); 79 | result.Add("current_vdf_sub_slot_iters", state.SubSlotIters); 80 | 81 | var totalIters = state.Peak is not null ? state.Peak.TotalIters : 0; 82 | result.Add("total_iterations_since_start", totalIters); 83 | 84 | output.WriteOutput(result); 85 | 86 | if (!Json && Verbose && About) 87 | { 88 | AnsiConsole.Write(new FigletText("GO CHIA!") 89 | .Centered() 90 | .Color(Color.Green)); 91 | } 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/rchia/Commands/AttributeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.CommandLine; 4 | using System.CommandLine.Builder; 5 | using System.CommandLine.Invocation; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace rchia.Commands; 10 | 11 | public static class AttributeExtensions 12 | { 13 | public static CommandLineBuilder UseAttributes(this CommandLineBuilder builder, Assembly assembly) 14 | { 15 | var types = from type in assembly.GetTypes() 16 | let attr = type.GetCustomAttribute() 17 | where attr is not null 18 | orderby attr.Name 19 | select (type, attr); 20 | 21 | var root = builder.Command; 22 | foreach (var (type, attr) in types) 23 | { 24 | root.AddCommands(type, attr); 25 | } 26 | 27 | return builder; 28 | } 29 | 30 | private static void AddCommands(this System.CommandLine.Command parent, Type type, CommandAttribute c) 31 | { 32 | var command = new System.CommandLine.Command(c.Name, c.Description); 33 | 34 | // get all the options for the command 35 | foreach (var (property, attr) in type.GetAttributedProperties()) 36 | { 37 | var aliases = new List(); 38 | if (!string.IsNullOrEmpty(attr.ShortName)) 39 | { 40 | aliases.Add($"-{attr.ShortName}"); 41 | } 42 | 43 | if (!string.IsNullOrEmpty(attr.LongName)) 44 | { 45 | aliases.Add($"--{attr.LongName}"); 46 | } 47 | 48 | var option = new Option(aliases.ToArray(), attr.Description, property.PropertyType) 49 | { 50 | ArgumentHelpName = attr.ArgumentHelpName ?? property.Name, 51 | IsRequired = attr.IsRequired, 52 | IsHidden = attr.IsHidden 53 | }; 54 | 55 | if (attr.Default is not null) 56 | { 57 | option.SetDefaultValue(attr.Default); 58 | } 59 | 60 | command.AddOption(option); 61 | } 62 | 63 | // add required arguments 64 | foreach (var (property, attr) in type.GetAttributedProperties().OrderBy(tuple => tuple.Attribute.Index)) 65 | { 66 | var argument = new Argument(attr.Name) 67 | { 68 | ArgumentType = property.PropertyType, 69 | }; 70 | 71 | if (attr.Default is not null) 72 | { 73 | argument.SetDefaultValue(attr.Default); 74 | } 75 | 76 | argument.Description = attr.Description; 77 | command.AddArgument(argument); 78 | } 79 | 80 | // and recurse to add subcommands 81 | foreach (var (property, subcommand) in type.GetAttributedProperties()) 82 | { 83 | command.AddCommands(property.PropertyType, subcommand); 84 | } 85 | 86 | var target = type.GetCommandTarget(); // ?? throw new InvalidOperationException($"No method decorated with [CommandTarget] was found on {type.FullName}"); 87 | if (target is not null) 88 | { 89 | command.Handler = CommandHandler.Create(target); 90 | } 91 | 92 | parent.AddCommand(command); 93 | } 94 | 95 | public static MethodInfo? GetCommandTarget(this Type type) 96 | { 97 | var targets = from method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public) 98 | let attr = method.GetCustomAttribute(true) 99 | where attr is not null 100 | select method; 101 | 102 | return targets.FirstOrDefault(); 103 | } 104 | 105 | private static IEnumerable<(PropertyInfo Propety, T Attribute)> GetAttributedProperties(this Type type) where T : Attribute 106 | { 107 | return from property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) 108 | let attr = property.GetCustomAttribute() 109 | where attr is not null 110 | select (property, attr); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/rchia/ClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using chia.dotnet; 5 | 6 | namespace rchia; 7 | 8 | internal class ClientFactory 9 | { 10 | public static void Initialize(string originService) 11 | { 12 | Factory = new(originService); 13 | } 14 | 15 | public static ClientFactory Factory { get; private set; } = new("not_set"); 16 | 17 | private ClientFactory(string originService) 18 | { 19 | OriginService = originService; 20 | } 21 | 22 | public string OriginService { get; init; } // this is needed for any daemon wss endpoints 23 | 24 | public async Task TestConnection(EndpointInfo endpoint, int timeoutMilliseconds) 25 | { 26 | using var rpcClient = await CreateRpcClient(endpoint, timeoutMilliseconds); 27 | } 28 | 29 | public async Task CreateRpcClient(ICommandOutput output, EndpointOptions options, string serviceName) 30 | { 31 | var endpoint = GetEndpointInfo(output, options, serviceName); 32 | using var message = new StatusMessage(output.Status, $"Connecting to endpoint {endpoint.Uri}..."); 33 | 34 | return await CreateRpcClient(endpoint, options.TimeoutMilliseconds); 35 | } 36 | 37 | private async Task CreateRpcClient(EndpointInfo endpoint, int timeoutMilliseconds) 38 | { 39 | return endpoint.Uri.Scheme == "wss" 40 | ? await CreateWebSocketClient(endpoint, timeoutMilliseconds) 41 | : endpoint.Uri.Scheme == "https" 42 | ? new HttpRpcClient(endpoint) 43 | : throw new InvalidOperationException($"Unrecognized endpoint Uri scheme {endpoint.Uri.Scheme}"); 44 | } 45 | 46 | public async Task CreateWebSocketClient(ICommandOutput output, EndpointOptions options) 47 | { 48 | var endpoint = GetEndpointInfo(output, options, ServiceNames.Daemon); 49 | 50 | if (endpoint.Uri.Scheme != "wss") 51 | { 52 | throw new InvalidOperationException($"Expecting a daemon endpoint using the websocket protocol but found {endpoint.Uri}"); 53 | } 54 | using var message = new StatusMessage(output.Status, $"Connecting to websocket {endpoint.Uri}..."); 55 | 56 | return await CreateWebSocketClient(endpoint, options.TimeoutMilliseconds); 57 | } 58 | 59 | private async Task CreateWebSocketClient(EndpointInfo endpoint, int timeoutMilliseconds) 60 | { 61 | using var cts = new CancellationTokenSource(timeoutMilliseconds); 62 | 63 | var rpcClient = new WebSocketRpcClient(endpoint); 64 | await rpcClient.Connect(cts.Token); 65 | 66 | var daemon = new DaemonProxy(rpcClient, OriginService); 67 | await daemon.RegisterService(cts.Token); 68 | 69 | return rpcClient; 70 | } 71 | 72 | private static EndpointInfo GetEndpointInfo(ICommandOutput output, EndpointOptions options, string serviceName) 73 | { 74 | var library = EndpointLibrary.OpenLibrary(); 75 | 76 | if (!string.IsNullOrEmpty(options.Endpoint)) 77 | { 78 | return !library.Endpoints.ContainsKey(options.Endpoint) 79 | ? throw new InvalidOperationException($"There is no saved endpoint {options.Endpoint}") 80 | : library.Endpoints[options.Endpoint].EndpointInfo; 81 | } 82 | 83 | if (options.DefaultConfig) 84 | { 85 | return Config.Open().GetEndpoint(serviceName); 86 | } 87 | 88 | if (options.ConfigPath is not null) 89 | { 90 | return Config.Open(options.ConfigPath.FullName).GetEndpoint(serviceName); 91 | } 92 | 93 | if (!string.IsNullOrEmpty(options.EndpointUri)) 94 | { 95 | return options.CertPath is null || options.KeyPath is null 96 | ? throw new InvalidOperationException("When a --endpoint-uri is set both --key-path and --cert-path must also be set") 97 | : new EndpointInfo() 98 | { 99 | Uri = new Uri(options.EndpointUri), 100 | CertPath = options.CertPath.FullName, 101 | KeyPath = options.KeyPath.FullName 102 | }; 103 | } 104 | 105 | if (!options.DefaultEndpoint) 106 | { 107 | output.WriteMessage("No endpoint options set. Using default endpoint."); 108 | } 109 | 110 | var endpoint = library.GetDefault(); 111 | return endpoint.EndpointInfo; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/rchia/Commands/ConsoleOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Numerics; 6 | using Spectre.Console; 7 | 8 | namespace rchia.Commands; 9 | 10 | internal class ConsoleOutput : ICommandOutput 11 | { 12 | private StatusContext? _statusContext; 13 | 14 | public bool Verbose { get; init; } 15 | 16 | public IStatus Status => new ConsoleStatus(_statusContext); 17 | 18 | public ICommandOutput SetContext(StatusContext? context) 19 | { 20 | _statusContext = context; 21 | return this; 22 | } 23 | 24 | public void WriteOutput(IDictionary>> output) 25 | { 26 | foreach (var table in output) 27 | { 28 | WriteTable(table.Value, table.Key); 29 | } 30 | } 31 | 32 | public void WriteOutput(string name, object? value, bool verbose) 33 | { 34 | if (verbose) 35 | { 36 | WriteMarkupLine($"[wheat1]{name.FromSnakeCase()}:[/] {value}"); 37 | } 38 | else 39 | { 40 | WriteLine($"{value}"); 41 | } 42 | } 43 | 44 | public void WriteOutput(object output) 45 | { 46 | WriteLine(output.ToJson()); 47 | } 48 | 49 | public void WriteOutput(IEnumerable> output) 50 | { 51 | WriteTable(output, string.Empty); 52 | } 53 | 54 | public void WriteOutput(IEnumerable output) 55 | { 56 | foreach (var value in output) 57 | { 58 | WriteLine(value); 59 | } 60 | } 61 | 62 | public void WriteOutput(IDictionary output) 63 | { 64 | foreach (var value in output) 65 | { 66 | WriteMarkupLine($"[wheat1]{value.Key.FromSnakeCase()}:[/] {Format(value.Value)}"); 67 | } 68 | } 69 | 70 | private static object? Format(object? value) 71 | { 72 | if (value is null) 73 | { 74 | return null; 75 | } 76 | if (value.GetType() == typeof(ulong)) 77 | { 78 | return ((ulong)value).ToString("N0"); 79 | } 80 | if (value.GetType() == typeof(uint)) 81 | { 82 | return ((uint)value).ToString("N0"); 83 | } 84 | if (value.GetType() == typeof(BigInteger)) 85 | { 86 | return ((BigInteger)value).ToString("N0"); 87 | } 88 | return value; 89 | } 90 | 91 | private static void WriteTable(IEnumerable> output, string title) 92 | { 93 | var first = output.FirstOrDefault(); 94 | if (first != null) 95 | { 96 | var table = new Table(); 97 | 98 | if (!string.IsNullOrEmpty(title)) 99 | { 100 | table.Title = new TableTitle($"[orange3]{title.FromSnakeCase()}[/]"); 101 | } 102 | 103 | // the first item holds the column names - it is assumed all items have the same keys 104 | foreach (var column in first.Keys) 105 | { 106 | table.AddColumn($"[orange3]{column.FromSnakeCase()}[/]"); 107 | } 108 | 109 | // now add the values from all the rows 110 | foreach (var row in output) 111 | { 112 | table.AddRow(row.Values.Select(item => $"{Format(item)}").ToArray()); 113 | } 114 | 115 | AnsiConsole.Write(table); 116 | } 117 | } 118 | 119 | public void WriteMarkupLine(string msg) 120 | { 121 | AnsiConsole.MarkupLine(msg); 122 | } 123 | 124 | public void WriteLine(string msg) 125 | { 126 | AnsiConsole.WriteLine(msg); 127 | } 128 | 129 | public void WriteMessage(string msg, bool important = false) 130 | { 131 | if (important) 132 | { 133 | WriteMarkupLine($"[yellow]{msg}[/]"); 134 | } 135 | else if (Verbose) 136 | { 137 | WriteMarkupLine(msg); 138 | } 139 | 140 | Debug.WriteLine(msg); 141 | } 142 | 143 | public void WriteWarning(string msg) 144 | { 145 | WriteMarkupLine($"[yellow]{msg}[/]"); 146 | } 147 | 148 | public void WriteError(Exception e) 149 | { 150 | if (Verbose) 151 | { 152 | AnsiConsole.WriteException(e, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); 153 | } 154 | else 155 | { 156 | WriteMarkupLine($"[red]{e.Message}[/]"); 157 | } 158 | } 159 | 160 | public bool Confirm(string warning, bool force) 161 | { 162 | if (!force) 163 | { 164 | if (!AnsiConsole.Confirm(warning, false)) 165 | { 166 | WriteMessage("Cancelled"); 167 | return false; 168 | } 169 | 170 | WriteMessage("Confirmed"); 171 | } 172 | 173 | return true; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/rchia/Blocks/BlockHeaderCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using chia.dotnet; 6 | using chia.dotnet.bech32; 7 | using rchia.Commands; 8 | 9 | namespace rchia.Blocks; 10 | 11 | internal sealed class BlockHeaderCommand : EndpointOptions 12 | { 13 | [Option("a", "hash", ArgumentHelpName = "HASH", Description = "Look up a block by header hash")] 14 | public string? Hash { get; init; } 15 | 16 | [Option("h", "height", ArgumentHelpName = "HEIGHT", Description = "Look up a block by height")] 17 | public uint? Height { get; init; } 18 | 19 | private async Task GetHash(FullNodeProxy proxy) 20 | { 21 | if (Height.HasValue) 22 | { 23 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 24 | var block = await proxy.GetBlockRecordByHeight(Height.Value, cts.Token); 25 | 26 | return block.HeaderHash; 27 | } 28 | 29 | if (!string.IsNullOrEmpty(Hash)) 30 | { 31 | return Hash; 32 | } 33 | 34 | throw new InvalidOperationException("Either a valid block height or header hash must be specified."); 35 | } 36 | 37 | [CommandTarget] 38 | public async Task Run() 39 | { 40 | return await DoWorkAsync("Retrieving block header...", async output => 41 | { 42 | using var rpcClient = await ClientFactory.Factory.CreateRpcClient(output, this, ServiceNames.FullNode); 43 | var proxy = new FullNodeProxy(rpcClient, ClientFactory.Factory.OriginService); 44 | var headerHash = await GetHash(proxy); 45 | 46 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 47 | var full_block = await proxy.GetBlock(headerHash, cts.Token); 48 | var block = await proxy.GetBlockRecord(headerHash, cts.Token); 49 | 50 | if (Json) 51 | { 52 | var dict = new Dictionary() 53 | { 54 | { "block", block }, 55 | { "full_block", full_block }, 56 | 57 | }; 58 | output.WriteOutput(dict); 59 | } 60 | else 61 | { 62 | var (NetworkName, NetworkPrefix) = await proxy.GetNetworkInfo(cts.Token); 63 | var previous = await proxy.GetBlockRecord(block.PrevHash, cts.Token); 64 | 65 | var result = new Dictionary 66 | { 67 | { "block_height", block.Height }, 68 | { "header_hash", new Formattable(block.HeaderHash, hash => hash.Replace("0x", string.Empty)) } 69 | }; 70 | 71 | var timestamp = block.DateTimestamp.HasValue ? block.DateTimestamp.Value.ToLocalTime().ToString() : "Not a transaction block"; 72 | result.Add("timestamp", timestamp); 73 | result.Add("weight", block.Weight); 74 | result.Add("previous_block", new Formattable(block.PrevHash, hash => hash.Replace("0x", string.Empty))); 75 | 76 | var difficulty = previous is not null ? block.Weight - previous.Weight : block.Weight; 77 | result.Add("difficulty", difficulty); 78 | result.Add("subslot_iters", block.SubSlotIters); 79 | result.Add("cost", full_block.TransactionsInfo?.Cost); 80 | result.Add("total_vdf_iterations", block.TotalIters); 81 | result.Add("is_transaction_block", full_block.RewardChainBlock.IsTransactionBlock); 82 | result.Add("deficit", block.Deficit); 83 | result.Add("k_size", full_block.RewardChainBlock.ProofOfSpace.Size); 84 | result.Add("plot_public_key", full_block.RewardChainBlock.ProofOfSpace.PlotPublicKey); 85 | 86 | var poolPk = full_block.RewardChainBlock.ProofOfSpace.PublicPoolKey; 87 | result.Add("pool_public_key", string.IsNullOrEmpty(poolPk) ? "Pay to pool puzzle hash" : poolPk); 88 | 89 | var txFilterHash = full_block.FoliageTransactionBlock is not null ? full_block.FoliageTransactionBlock.FilterHash : "Not a transaction block"; 90 | result.Add("tx_filter_hash", new Formattable(txFilterHash, hash => hash.Replace("0x", string.Empty))); 91 | 92 | var bech32 = new Bech32M(NetworkPrefix); 93 | var farmerAddress = bech32.PuzzleHashToAddress(block.FarmerPuzzleHash.Replace("0x", string.Empty)); 94 | var poolAddress = bech32.PuzzleHashToAddress(block.PoolPuzzleHash.Replace("0x", string.Empty)); 95 | 96 | result.Add("farmer_address", farmerAddress); 97 | result.Add("pool_address", poolAddress); 98 | 99 | var fees = block.Fees.HasValue ? block.Fees.Value.ToString() : "Not a transaction block"; 100 | result.Add("fees_amount", fees); 101 | 102 | output.WriteOutput(result); 103 | } 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rchia 2 | 3 | Remote management CLI for [chia nodes](https://github.com/Chia-Network/chia-blockchain). 4 | 5 | [![.NET](https://github.com/dkackman/rchia/actions/workflows/dotnet.yml/badge.svg)](https://github.com/dkackman/rchia/actions/workflows/dotnet.yml) 6 | [![CodeQL](https://github.com/dkackman/rchia/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/dkackman/rchia/actions/workflows/codeql-analysis.yml) 7 | 8 | ## Introduction 9 | 10 | A cross platform (Linux ARM & x64, Windows, MacOS) command line utility that mirrors the `chia` CLI, but uses RPC rather than running locally on the node. This allows management of any number of nodes from a central location, as long as [their RPC interface is exposed on the network](https://github.com/dkackman/rchia/wiki/Exposing-a-Node-on-the-Network). 11 | 12 | ## Getting Started 13 | 14 | - [Exposing a node on the network](https://github.com/dkackman/rchia/wiki/Exposing-a-Node-on-the-Network) 15 | - [Specifying an endpoint on the command line](https://github.com/dkackman/rchia/wiki/Specifiying-RPC-Endpoints) 16 | - [Saving up endpoint definitions](https://github.com/dkackman/rchia/wiki/Managing-Saved-Enpoints) 17 | 18 | ## Build and Run 19 | 20 | Install [the .net6 sdk](https://dotnet.microsoft.com/download) 21 | 22 | ```bash 23 | dotnet build src 24 | cd src/rchia/bin/Debug/net6.0/ 25 | ./rchia --help 26 | ``` 27 | 28 | ## Install 29 | 30 | Download the appropriate installer from [the latest release](https://github.com/dkackman/rchia/releases). 31 | There are three downloads types for each OS: 32 | - `standalone` - the dotnet framework is bundled in the executable. Large file but no dependencies 33 | - `singlefile` - the executable and its dependencies are bundled as a single file. Smaller file but requires the [dotnet 6 runtime](https://dotnet.microsoft.com/download/dotnet/6.0) 34 | - `any-cpu` - the executable and its dependencies are not bundled together. Smaller file but requires the [dotnet 6 runtime](https://dotnet.microsoft.com/download/dotnet/6.0) 35 | 36 | _on non-windows you will need to `chmod +x` the executable (either `rchia` or `rchia.exe` depending on the build target)_ 37 | 38 | ## Example 39 | 40 | Incorporates Ansi console features when available. 41 | ![image](https://user-images.githubusercontent.com/5160233/134552277-59128c00-64e0-474d-88ac-50b092993a68.png) 42 | 43 | The details of the endpoint can be specified in the following ways: 44 | 45 | ### On the command line 46 | 47 | ```bash 48 | ./rchia node status --endpoint-uri https://node1:8555 --cert-path ~/certs/node1/private_full_node.crt --key-path ~/certs/node1/private_full_node.key 49 | ``` 50 | 51 | ### By using the `chia` config 52 | 53 | ```bash 54 | ./rchia node status --default-chia-config 55 | ``` 56 | 57 | ### Using saved endpoint connections 58 | 59 | ```bash 60 | ./rchia endpoints --add node1 https://node1:8555 ~/certs/node1/private_full_node.crt ~/certs/node1/private_full_node.key 61 | ./rchia node status --endpoint node1 62 | ``` 63 | 64 | ### Suported Verbs 65 | 66 | ```bash 67 | rchia 68 | 69 | Usage: 70 | rchia [options] [command] 71 | 72 | Options: 73 | --version Show version information 74 | -?, -h, --help Show help and usage information 75 | 76 | Commands: 77 | bech32 Convert addresses to and from puzzle hashes. 78 | blocks Show informations about blocks and coins. 79 | Requires a daemon or full_node endpoint. 80 | connections Various methods for managing node connections. 81 | Requires a daemon or full_node endpoint. 82 | endpoints Manage saved endpoints. 83 | farm Manage your farm. 84 | Requires a daemon endpoint. 85 | keys Manage your keys 86 | Requires a wallet or daemon endpoint. 87 | netspace Calculates the estimated space on the network given two block header hashes. 88 | Requires a daemon or full_node endpoint. 89 | node Commands to managing the node. 90 | Requires a daemon endpoint. 91 | plotnft Manage your plot NFTs. 92 | Requires a wallet or daemon endpoint. 93 | plots Manage your plots. 94 | Requires a harvester, plotter or daemon endpoint. 95 | services Shows the status of the node. 96 | Requires a daemon endpoint. 97 | wallets Manage your wallet. 98 | Requires a wallet or daemon endpoint. 99 | ``` 100 | 101 | ### Json Output 102 | 103 | All command support returning JSON instead of formatted output with a `--json` flag: 104 | 105 | ```bash 106 | ./rchia wallets show -ep my_wallet --json 107 | { 108 | "summary": { 109 | "fingerprint": "2287630151", 110 | "sync_status": "Synced", 111 | "wallet_height": 101725 112 | }, 113 | "wallets": [ 114 | { 115 | "Id": 1, 116 | "Name": "Chia Wallet", 117 | "Type": "STANDARD_WALLET", 118 | "Total": 0.792643692567, 119 | "Pending Total": 0.792643692567, 120 | "Spendable": 0.792643692567, 121 | "Pending Change": 0.0, 122 | "Max Spend Amount": 0.792643692567, 123 | "Unspent Coin Count": 68, 124 | "Pending Coin Removal Count": 0 125 | }, 126 | { 127 | "Id": 2, 128 | "Name": "Pool wallet", 129 | "Type": "POOLING_WALLET", 130 | "Total": 0.0, 131 | "Pending Total": 0.0, 132 | "Spendable": 0.0, 133 | "Pending Change": 0.0, 134 | "Max Spend Amount": 0.0, 135 | "Unspent Coin Count": 0, 136 | "Pending Coin Removal Count": 0 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | ### Tabular output 143 | 144 | ![image](https://user-images.githubusercontent.com/5160233/134552904-50ea4822-d53a-4144-85be-86c9bcbd1625.png) 145 | ___ 146 | 147 | _chia and its logo are the registered trademark or trademark of Chia Network, Inc. in the United States and worldwide._ 148 | -------------------------------------------------------------------------------- /src/publish-all.ps1: -------------------------------------------------------------------------------- 1 | $version = "0.9.1" 2 | 3 | # clean build and publish outputs 4 | Remove-Item '.\rchia\bin' -Recurse -Force 5 | Remove-Item '.\publish' -Recurse -Force 6 | 7 | # stand alone 8 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/standalone/win-x64 --self-contained True --runtime win-x64 --verbosity Normal /property:PublishTrimmed=True /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 9 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/standalone/linux-x64 --self-contained True --runtime linux-x64 --verbosity Normal /property:PublishTrimmed=True /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 10 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/standalone/osx.11.0-x64 --self-contained True --runtime osx.11.0-x64 --verbosity Normal /property:PublishTrimmed=True /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 11 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/standalone/linux-arm64 --self-contained True --runtime linux-arm64 --verbosity Normal /property:PublishTrimmed=True /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 12 | 13 | Compress-Archive -CompressionLevel Optimal -Path publish/standalone/win-x64/* -DestinationPath publish/rchia-$version-standalone-win-x64.zip 14 | Compress-Archive -CompressionLevel Optimal -Path publish/standalone/linux-x64/* -DestinationPath publish/rchia-$version-standalone-linux-x64.zip 15 | Compress-Archive -CompressionLevel Optimal -Path publish/standalone/osx.11.0-x64/* -DestinationPath publish/rchia-$version-standalone-osx.11.0-x64.zip 16 | Compress-Archive -CompressionLevel Optimal -Path publish/standalone/linux-arm64/* -DestinationPath publish/rchia-$version-standalone-linux-arm64.zip 17 | 18 | # single file but requires dotnet 19 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/singlefile/win-x64 --self-contained False --runtime win-x64 --verbosity Normal /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 20 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/singlefile/linux-x64 --self-contained False --runtime linux-x64 --verbosity Normal /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 21 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/singlefile/osx.11.0-x64 --self-contained False --runtime osx.11.0-x64 --verbosity Normal /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 22 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/singlefile/linux-arm64 --self-contained False --runtime linux-arm64 --verbosity Normal /property:PublishSingleFile=True /property:IncludeNativeLibrariesForSelfExtract=True /property:DebugType=None /property:DebugSymbols=False 23 | 24 | Compress-Archive -CompressionLevel Optimal -Path publish/singlefile/win-x64/* -DestinationPath publish/rchia-$version-singlefile-win-x64.zip 25 | Compress-Archive -CompressionLevel Optimal -Path publish/singlefile/linux-x64/* -DestinationPath publish/rchia-$version-singlefile-linux-x64.zip 26 | Compress-Archive -CompressionLevel Optimal -Path publish/singlefile/osx.11.0-x64/* -DestinationPath publish/rchia-$version-singlefile-osx.11.0-x64.zip 27 | Compress-Archive -CompressionLevel Optimal -Path publish/singlefile/linux-arm64/* -DestinationPath publish/rchia-$version-singlefile-linux-arm64.zip 28 | 29 | # files 30 | # dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/files/win-x64 --self-contained False --runtime win-x64 --verbosity Normal /property:PublishSingleFile=False /property:DebugType=None /property:DebugSymbols=False 31 | # dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/files/linux-x64 --self-contained False --runtime linux-x64 --verbosity Normal /property:PublishSingleFile=False /property:DebugType=None /property:DebugSymbols=False 32 | # dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/files/osx.11.0-x64 --self-contained False --runtime osx.11.0-x64 --verbosity Normal /property:PublishSingleFile=False /property:DebugType=None /property:DebugSymbols=False 33 | # dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/files/linux-arm64 --self-contained False --runtime linux-arm64 --verbosity Normal /property:PublishSingleFile=False /property:DebugType=None /property:DebugSymbols=False 34 | dotnet publish ./rchia/rchia.csproj --configuration Release --framework net6.0 --output publish/files/any-cpu --self-contained False --verbosity Normal /property:PublishSingleFile=False /property:DebugType=None /property:DebugSymbols=False 35 | 36 | # Compress-Archive -CompressionLevel Optimal -Path publish/files/win-x64/* -DestinationPath publish/rchia-$version-files-win-x64.zip 37 | # Compress-Archive -CompressionLevel Optimal -Path publish/files/linux-x64/* -DestinationPath publish/rchia-$version-files-linux-x64.zip 38 | # Compress-Archive -CompressionLevel Optimal -Path publish/files/osx.11.0-x64/* -DestinationPath publish/rchia-$version-files-osx.11.0-x64.zip 39 | # Compress-Archive -CompressionLevel Optimal -Path publish/files/linux-arm64/* -DestinationPath publish/rchia-$version-files-linux-arm64.zip 40 | Compress-Archive -CompressionLevel Optimal -Path publish/files/any-cpu/* -DestinationPath publish/rchia-$version-files-any-cpu.zip 41 | 42 | #nuget 43 | Copy-Item ./rchia/bin/release/rchia.$version.nupkg -Destination ./publish 44 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Users\dkack\src\github\dkackman\rchia\src codebase based on best match to current usage at 9/6/2021 2 | # You can modify the rules from these initially generated values to suit your own policies 3 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 4 | [*.cs] 5 | 6 | 7 | #Core editorconfig formatting - indentation 8 | 9 | #use soft tabs (spaces) for indentation 10 | indent_style = space 11 | 12 | #Formatting - new line options 13 | 14 | #place catch statements on a new line 15 | csharp_new_line_before_catch = true 16 | #place else statements on a new line 17 | csharp_new_line_before_else = true 18 | #require members of object intializers to be on separate lines 19 | csharp_new_line_before_members_in_object_initializers = true 20 | #require braces to be on a new line for methods, object_collection_array_initializers, control_blocks, types, and lambdas (also known as "Allman" style) 21 | csharp_new_line_before_open_brace =all 22 | #require elements of query expression clauses to be on separate lines 23 | csharp_new_line_between_query_expression_clauses = true 24 | 25 | #Formatting - organize using options 26 | 27 | #sort System.* using directives alphabetically, and place them before other usings 28 | dotnet_sort_system_directives_first = true 29 | 30 | #Formatting - spacing options 31 | 32 | #require NO space between a cast and the value 33 | csharp_space_after_cast = false 34 | #require a space before the colon for bases or interfaces in a type declaration 35 | csharp_space_after_colon_in_inheritance_clause = true 36 | #require a space after a keyword in a control flow statement such as a for loop 37 | csharp_space_after_keywords_in_control_flow_statements = true 38 | #require a space before the colon for bases or interfaces in a type declaration 39 | csharp_space_before_colon_in_inheritance_clause = true 40 | #remove space within empty argument list parentheses 41 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 42 | #remove space between method call name and opening parenthesis 43 | csharp_space_between_method_call_name_and_opening_parenthesis = false 44 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 45 | csharp_space_between_method_call_parameter_list_parentheses = false 46 | #remove space within empty parameter list parentheses for a method declaration 47 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 48 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 49 | csharp_space_between_method_declaration_parameter_list_parentheses = false 50 | 51 | #Formatting - wrapping options 52 | 53 | #leave code block on single line 54 | csharp_preserve_single_line_blocks = true 55 | #leave statements and member declarations on the same line 56 | csharp_preserve_single_line_statements = true 57 | 58 | #Style - Code block preferences 59 | 60 | #prefer curly braces even for one line of code 61 | csharp_prefer_braces = true:suggestion 62 | 63 | #Style - expression bodied member options 64 | 65 | #prefer block bodies for constructors 66 | csharp_style_expression_bodied_constructors = false:suggestion 67 | #prefer block bodies for methods 68 | csharp_style_expression_bodied_methods = false:suggestion 69 | #prefer block bodies for operators 70 | csharp_style_expression_bodied_operators = false:suggestion 71 | #prefer expression-bodied members for properties 72 | csharp_style_expression_bodied_properties = true:suggestion 73 | 74 | #Style - expression level options 75 | 76 | #prefer out variables to be declared inline in the argument list of a method call when possible 77 | csharp_style_inlined_variable_declaration = true:suggestion 78 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 79 | dotnet_style_predefined_type_for_member_access = true:suggestion 80 | 81 | #Style - Expression-level preferences 82 | 83 | #prefer objects to be initialized using object initializers when possible 84 | dotnet_style_object_initializer = true:suggestion 85 | #prefer inferred tuple element names 86 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 87 | 88 | #Style - implicit and explicit types 89 | 90 | #prefer var over explicit type in all cases, unless overridden by another code style rule 91 | csharp_style_var_elsewhere = true:suggestion 92 | #prefer var is used to declare variables with built-in system types such as int 93 | csharp_style_var_for_built_in_types = true:suggestion 94 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 95 | csharp_style_var_when_type_is_apparent = true:suggestion 96 | 97 | #Style - language keyword and framework type options 98 | 99 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 100 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 101 | 102 | #Style - modifier options 103 | 104 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 105 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 106 | 107 | #Style - Modifier preferences 108 | 109 | #when this rule is set to a list of modifiers, prefer the specified ordering. 110 | csharp_preferred_modifier_order = public,private,internal,protected,async,static,override,sealed,abstract,readonly:suggestion 111 | 112 | #Style - Pattern matching 113 | 114 | #prefer pattern matching instead of is expression with type casts 115 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 116 | 117 | #Style - qualification options 118 | 119 | #prefer methods not to be prefaced with this. or Me. in Visual Basic 120 | dotnet_style_qualification_for_method = false:suggestion 121 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 122 | dotnet_style_qualification_for_property = false:suggestion 123 | dotnet_diagnostic.CA1507.severity=warning 124 | dotnet_diagnostic.CA1812.severity=suggestion 125 | insert_final_newline=true 126 | 127 | [*.{cs,vb}] 128 | dotnet_diagnostic.CA1708.severity=warning 129 | dotnet_diagnostic.CA1712.severity=warning -------------------------------------------------------------------------------- /src/rchia/PlotNft/ShowPlotNftCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using chia.dotnet; 7 | using chia.dotnet.bech32; 8 | using rchia.Commands; 9 | 10 | namespace rchia.PlotNft; 11 | 12 | internal sealed class ShowPlotNftCommand : WalletCommand 13 | { 14 | [Option("i", "id", Default = 1, Description = "Id of the user wallet to use")] 15 | public uint Id { get; init; } = 1; 16 | 17 | [CommandTarget] 18 | public async Task Run() 19 | { 20 | return await DoWorkAsync("Retrieving pool info...", async output => 21 | { 22 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 23 | var daemon = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 24 | var farmer = daemon.CreateProxyFrom(); 25 | var wallet = daemon.CreateProxyFrom(); 26 | 27 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 28 | var allWallets = await wallet.GetWallets(cts.Token); 29 | var poolingWallets = allWallets.Where(w => w.Type == WalletType.POOLING_WALLET); 30 | if (!poolingWallets.Any()) 31 | { 32 | throw new InvalidOperationException("There are no pooling wallets on this node."); 33 | } 34 | 35 | // since the command specified a specific wallet narrow the result to just that 36 | var walletInfo = poolingWallets.FirstOrDefault(w => w.Id == Id); 37 | if (walletInfo is null) 38 | { 39 | throw new InvalidOperationException($"Wallet with id: {Id} is not a pooling wallet or doesn't exist. Please provide a different id."); 40 | } 41 | 42 | var poolStates = await farmer.GetPoolState(cts.Token); 43 | var harvesters = await farmer.GetHarvesters(cts.Token); 44 | var (NetworkName, NetworkPrefix) = await wallet.GetNetworkInfo(cts.Token); 45 | var height = await wallet.GetHeightInfo(cts.Token); 46 | var (GenesisInitialized, Synced, Syncing) = await wallet.GetSyncStatus(cts.Token); 47 | 48 | var result = new Dictionary() 49 | { 50 | { "wallet_height", height }, 51 | { "sync_status", $"{(Synced ? string.Empty : "not ")}synced"}, 52 | { "wallet", walletInfo.Id } 53 | }; 54 | 55 | var poolwallet = new PoolWallet(walletInfo.Id, wallet); 56 | using var cts1 = new CancellationTokenSource(TimeoutMilliseconds); 57 | var (State, UnconfirmedTransactions) = await poolwallet.Status(cts1.Token); 58 | 59 | if (State.Current.State == PoolSingletonState.LEAVING_POOL) 60 | { 61 | var expected = State.SingletonBlockHeight - State.Current.RelativeLockHeight; 62 | output.WriteWarning($"Current state: INVALID_STATE. Please leave/join again after block height {expected}"); 63 | } 64 | else 65 | { 66 | result.Add($"current_state", State.Current.State); 67 | } 68 | 69 | var bech32 = new Bech32M(NetworkPrefix); 70 | result.Add($"current_state_from_block_height", State.SingletonBlockHeight); 71 | result.Add($"launcher_id", State.LauncherId); 72 | result.Add($"target_address", bech32.PuzzleHashToAddress(State.Current.TargetPuzzleHash)); 73 | 74 | var poolPlots = from h in harvesters 75 | from plots in h.Plots 76 | where plots.PoolContractPuzzleHash == State.P2SingletonPuzzleHash 77 | select plots; 78 | 79 | var plotCount = poolPlots.Count(); 80 | result.Add($"number_of_plots", plotCount); 81 | result.Add($"owner_public_key", State.Current.OwnerPubkey); 82 | result.Add($"pool_contract_address", $"{bech32.PuzzleHashToAddress(State.P2SingletonPuzzleHash)}"); 83 | 84 | if (State.Target is not null) 85 | { 86 | result.Add($"target_state", State.Target.State); 87 | result.Add($"target_pool_url", State.Target.PoolUrl); 88 | } 89 | 90 | if (State.Current.State == PoolSingletonState.SELF_POOLING) 91 | { 92 | var (ConfirmedWalletBalance, UnconfirmedWalletBalance, SpendableBalance, PendingChange, MaxSendAmount, UnspentCoinCount, PendingCoinRemovalCount) = await poolwallet.GetBalance(cts1.Token); 93 | result.Add($"claimable_balance", $"{ConfirmedWalletBalance.ToChia()} {NetworkPrefix}"); 94 | } 95 | else if (State.Current.State == PoolSingletonState.FARMING_TO_POOL) 96 | { 97 | result.Add($"current_pool_url", State.Current.PoolUrl ?? string.Empty); 98 | var poolstate = poolStates.FirstOrDefault(ps => ps.PoolConfig.LauncherId == State.LauncherId); 99 | if (poolstate is not null) 100 | { 101 | result.Add($"current_difficulty", poolstate.CurrentDifficulty); 102 | result.Add($"points_balance", poolstate.CurrentPoints); 103 | if (poolstate.PointsFound24h.Any()) 104 | { 105 | var pointsAcknowledged = poolstate.PointsAcknowledged24h.Count; 106 | var pct = pointsAcknowledged / (double)poolstate.PointsFound24h.Count; 107 | result.Add($"percent_successful_points_24h", $"{pct:P2}%"); 108 | } 109 | result.Add($"relative_lock_height", State.Current.RelativeLockHeight); 110 | try 111 | { 112 | result.Add("payout_instructions", bech32.PuzzleHashToAddress(poolstate.PoolConfig.PayoutInstructions)); 113 | } 114 | catch 115 | { 116 | result.Add("payout_instructions", poolstate.PoolConfig.PayoutInstructions); 117 | } 118 | } 119 | } 120 | else if (State.Current.State == PoolSingletonState.LEAVING_POOL) 121 | { 122 | var expected = State.SingletonBlockHeight - State.Current.RelativeLockHeight; 123 | if (State.Target is not null) 124 | { 125 | result.Add("predicted_leave_after_block_height", expected); 126 | } 127 | } 128 | 129 | output.WriteOutput(result); 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/rchia/Farm/SummaryCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using System.Numerics; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using chia.dotnet; 8 | using rchia.Commands; 9 | 10 | namespace rchia.Farm; 11 | 12 | internal sealed class SummaryCommand : EndpointOptions 13 | { 14 | [CommandTarget] 15 | public async Task Run() 16 | { 17 | return await DoWorkAsync("Retrieving farm info...", async output => 18 | { 19 | using var rpcClient = await ClientFactory.Factory.CreateWebSocketClient(output, this); 20 | var daemon = new DaemonProxy(rpcClient, ClientFactory.Factory.OriginService); 21 | var farmer = daemon.CreateProxyFrom(); 22 | var fullNode = daemon.CreateProxyFrom(); 23 | var wallet = daemon.CreateProxyFrom(); 24 | 25 | using var cts = new CancellationTokenSource(TimeoutMilliseconds); 26 | var all_harvesters = await farmer.GetHarvesters(cts.Token); 27 | var blockchain_state = await fullNode.GetBlockchainState(cts.Token); 28 | var farmer_running = await daemon.IsServiceRunning(ServiceNames.Farmer, cts.Token); 29 | var full_node_running = await daemon.IsServiceRunning(ServiceNames.FullNode, cts.Token); 30 | var wallet_running = await daemon.IsServiceRunning(ServiceNames.Wallet, cts.Token); 31 | 32 | var status = blockchain_state is null 33 | ? new Formattable("Not available", "grey") 34 | : blockchain_state.Sync.SyncMode 35 | ? new Formattable("Syncing", "orange3") 36 | : !blockchain_state.Sync.Synced 37 | ? new Formattable("Not running", "grey") 38 | : !farmer_running 39 | ? new Formattable("Not running", "grey") 40 | : new Formattable("Farming", "green"); 41 | 42 | var summary = new Dictionary 43 | { 44 | { "farming_status", status } 45 | }; 46 | 47 | var wallet_ready = true; 48 | if (wallet_running) 49 | { 50 | try 51 | { 52 | var (FarmedAmount, FarmerRewardAmount, FeeAmount, LastHeightFarmed, PoolRewardAmount) = await wallet.GetFarmedAmount(cts.Token); 53 | 54 | summary.Add("total_chia_farmed", FarmedAmount.ToChia()); 55 | summary.Add("user_transaction_fees", FeeAmount.ToChia()); 56 | summary.Add("block_rewards", (FarmerRewardAmount + PoolRewardAmount).ToChia()); 57 | summary.Add("last_height_farmed", LastHeightFarmed); 58 | } 59 | catch 60 | { 61 | wallet_ready = false; 62 | } 63 | } 64 | 65 | var harvesters = all_harvesters.ToList(); 66 | 67 | var totalPlotCount = 0; 68 | var totalplotSize = (ulong)0; 69 | 70 | var localHarvesters = harvesters.Where(h => h.Connection.IsLocal); 71 | var remoteHarvesters = harvesters.Where(h => !h.Connection.IsLocal); 72 | 73 | if (localHarvesters.Any()) 74 | { 75 | output.WriteMarkupLine($"[wheat1]Local Harvester{(localHarvesters.Count() == 1 ? string.Empty : 's')}[/]"); 76 | foreach (var harvester in localHarvesters) 77 | { 78 | var size = harvester.Plots.Sum(p => (double)p.FileSize); 79 | totalplotSize += (ulong)size; 80 | totalPlotCount += harvester.Plots.Count; 81 | 82 | output.WriteMarkupLine($" [green]{harvester.Connection.Host}[/]: [wheat1]{harvester.Plots.Count}[/] plots of size [wheat1]{size.ToBytesString("N1")}[/]"); 83 | } 84 | } 85 | 86 | if (remoteHarvesters.Any()) 87 | { 88 | output.WriteMarkupLine($"[wheat1]Remote Harvester{(remoteHarvesters.Count() == 1 ? string.Empty : 's')}[/]"); 89 | foreach (var harvester in remoteHarvesters) 90 | { 91 | var size = harvester.Plots.Sum(p => (double)p.FileSize); 92 | totalplotSize += (ulong)size; 93 | totalPlotCount += harvester.Plots.Count; 94 | 95 | output.WriteMarkupLine($" [green]{harvester.Connection.Host}:[/] [wheat1]{harvester.Plots.Count}[/] plots of size [wheat1]{size.ToBytesString("N1")}[/]"); 96 | } 97 | } 98 | 99 | summary.Add("total_plot_count", totalPlotCount); 100 | summary.Add("total_plot_size", new Formattable(totalplotSize, size => size.ToBytesString("N1"))); 101 | 102 | if (blockchain_state is not null) 103 | { 104 | summary.Add("estimated_network_space", new Formattable(blockchain_state.Space, space => space.ToBytesString())); 105 | } 106 | else 107 | { 108 | summary.Add("estimated_network_space", new Formattable("Unknown", "red")); 109 | } 110 | 111 | if (blockchain_state is not null && blockchain_state.Space != BigInteger.Zero) 112 | { 113 | if (totalPlotCount == 0) 114 | { 115 | summary.Add("expected_time_to_win", "Never (no plots)"); 116 | } 117 | else 118 | { 119 | var proportion = totalplotSize / (double)blockchain_state.Space; 120 | var blocktime = await fullNode.GetAverageBlockTime(cts.Token); 121 | var span = blocktime / proportion; 122 | 123 | summary.Add("expected_time_to_win", span.FormatTimeSpan()); 124 | output.WriteMessage($"Farming about {proportion:P6} percent of the network"); 125 | } 126 | } 127 | 128 | if (Json) 129 | { 130 | dynamic result = new ExpandoObject(); 131 | result.summary = summary.Sort(); 132 | result.local_harvesters = localHarvesters; 133 | result.remote_harvesters = remoteHarvesters; 134 | output.WriteOutput(result); 135 | } 136 | else 137 | { 138 | output.WriteOutput(summary); 139 | } 140 | 141 | if (!wallet_running) 142 | { 143 | output.WriteMessage("For details on farmed rewards and fees you should run '[grey]rchia start wallet[/]' and '[grey]rchia wallet show[/]'", true); 144 | } 145 | else if (!wallet_ready) 146 | { 147 | output.WriteMessage("For details on farmed rewards and fees you should run '[grey]rchia wallet show[/]'", true); 148 | } 149 | else 150 | { 151 | output.WriteMessage("Note: log into your key using '[grey]rchia wallet show[/]' to see rewards for each key", true); 152 | } 153 | }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------