diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..355cdfe
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,41 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+#### Define style ####
+
+# All files
+[*]
+indent_style = space
+
+# C# Project, JS and CSS files
+[*.{csproj,js,ts,css,scss}]
+indent_size = 2
+
+#### Suppress warnings ####
+
+# C# files
+[*.cs]
+
+# CA1056: Uri properties should not be strings
+dotnet_diagnostic.CA1056.severity = none
+
+# CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none # Don't need translated exceptions
+
+# CA1308: Normalize strings to uppercase
+dotnet_diagnostic.CA1308.severity = none # Also to lower is required
+
+# CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1707.severity = none # I like underscores into constants name
+
+# CA1812: Avoid uninstantiated internal classes
+dotnet_diagnostic.CA1812.severity = none # Doing extensive use of Dependency Injection
+
+# CA1822: Mark members as static
+dotnet_diagnostic.CA1822.severity = none # Don't like static members
+
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = none # .Net core doesn't require it
+
+# CA2234: Pass system uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = none
diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml
new file mode 100644
index 0000000..0e512e6
--- /dev/null
+++ b/.github/workflows/publish-stable.yml
@@ -0,0 +1,150 @@
+name: Publish stable release
+
+on:
+ push:
+ tags:
+ - 'v*.*.*'
+
+jobs:
+ release:
+ name: Release
+
+ strategy:
+ matrix:
+ kind: [
+ 'linux-x64',
+ 'linux-arm',
+ 'linux-arm64',
+
+ 'linux-selfcont-x64',
+ 'linux-selfcont-arm',
+ 'linux-selfcont-arm64',
+
+ 'macos-x64',
+ 'macos-arm64',
+
+ 'macos-selfcont-x64',
+ 'macos-selfcont-arm64',
+
+ 'windows-x86',
+ 'windows-x64',
+ 'windows-arm',
+ 'windows-arm64',
+
+ 'windows-selfcont-x86',
+ 'windows-selfcont-x64',
+ 'windows-selfcont-arm',
+ 'windows-selfcont-arm64']
+ include:
+ - kind: linux-x64
+ os: ubuntu-latest
+ target: linux-x64
+ frameworktype: --no-self-contained
+ - kind: linux-arm
+ os: ubuntu-latest
+ target: linux-arm
+ frameworktype: --no-self-contained
+ - kind: linux-arm64
+ os: ubuntu-latest
+ target: linux-arm64
+ frameworktype: --no-self-contained
+
+ - kind: linux-selfcont-x64
+ os: ubuntu-latest
+ target: linux-x64
+ frameworktype: --self-contained
+ - kind: linux-selfcont-arm
+ os: ubuntu-latest
+ target: linux-arm
+ frameworktype: --self-contained
+ - kind: linux-selfcont-arm64
+ os: ubuntu-latest
+ target: linux-arm64
+ frameworktype: --self-contained
+
+ - kind: macos-x64
+ os: macOS-latest
+ target: osx-x64
+ frameworktype: --no-self-contained
+ - kind: macos-arm64
+ os: macOS-latest
+ target: osx-arm64
+ frameworktype: --no-self-contained
+
+ - kind: macos-selfcont-x64
+ os: macOS-latest
+ target: osx-x64
+ frameworktype: --self-contained
+ - kind: macos-selfcont-arm64
+ os: macOS-latest
+ target: osx-arm64
+ frameworktype: --self-contained
+
+ - kind: windows-x86
+ os: windows-latest
+ target: win-x86
+ frameworktype: --no-self-contained
+ - kind: windows-x64
+ os: windows-latest
+ target: win-x64
+ frameworktype: --no-self-contained
+ - kind: windows-arm
+ os: windows-latest
+ target: win-arm
+ frameworktype: --no-self-contained
+ - kind: windows-arm64
+ os: windows-latest
+ target: win-arm64
+ frameworktype: --no-self-contained
+
+ - kind: windows-selfcont-x86
+ os: windows-latest
+ target: win-x86
+ frameworktype: --self-contained
+ - kind: windows-selfcont-x64
+ os: windows-latest
+ target: win-x64
+ frameworktype: --self-contained
+ - kind: windows-selfcont-arm
+ os: windows-latest
+ target: win-arm
+ frameworktype: --self-contained
+ - kind: windows-selfcont-arm64
+ os: windows-latest
+ target: win-arm64
+ frameworktype: --self-contained
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Build EthernaGatewayCli project
+ shell: bash
+ run: |
+ tag=$(git describe --tags --abbrev=0)
+ release_name="etherna-gateway-cli-$tag-${{ matrix.kind }}"
+ # Build everything
+ dotnet publish src/EthernaGatewayCli/EthernaGatewayCli.csproj --runtime "${{ matrix.target }}" "${{ matrix.frameworktype }}" -c Release -o "$release_name"
+ # Pack files
+ if [ "${{ matrix.kind }}" == windows* ]; then
+ 7z a -tzip "${release_name}.zip" "./${release_name}/*"
+ else
+ tar czvf "${release_name}.tar.gz" "$release_name"
+ fi
+
+ - name: Publish
+ uses: softprops/action-gh-release@v1
+ with:
+ files: |
+ etherna-gateway-cli-*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/EthernaGatewayCli.sln b/EthernaGatewayCli.sln
index 1660173..32dc09b 100644
--- a/EthernaGatewayCli.sln
+++ b/EthernaGatewayCli.sln
@@ -5,6 +5,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D3B7E18B-46F8-4072-9D14-801FDB15868C}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
+ nuget.config = nuget.config
+ .gitignore = .gitignore
+ .editorconfig = .editorconfig
EndProjectSection
EndProject
Global
diff --git a/EthernaGatewayCli.sln.DotSettings b/EthernaGatewayCli.sln.DotSettings
new file mode 100644
index 0000000..f73d392
--- /dev/null
+++ b/EthernaGatewayCli.sln.DotSettings
@@ -0,0 +1,17 @@
+
+ Copyright 2024-present Etherna SA
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ True
+ True
+ True
\ No newline at end of file
diff --git a/README.md b/README.md
index e93b7de..107186b 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,36 @@
A CLI interface to the Etherna Gateway
-## Issue reports
+## Instructions
+Download and extract binaries from [release page](https://github.com/Etherna/etherna-gateway-cli/releases).
+Etherna Gateway CLI requires at least [.NET 8 Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) and [ASP.NET Core 8 Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) installed on local machine to run, or it needs the `selfcontained` version of package, that already contains framework dependencies.
+
+### How to use
+
+```
+etherna
+A CLI interface to the Etherna Gateway
+
+Usage: etherna [OPTIONS] COMMAND
+
+Commands:
+ download Download a resource from Swarm
+ postage Manage postage batches
+ resource Manage Swarm resources
+ upload Upload a resource to Swarm
+
+Options:
+ -k, --api-key string Api Key (optional)
+ -i, --ignore-update Ignore new versions of EthernaGatewayCli
+
+Run 'etherna -h' or 'etherna --help' to print help.
+Run 'etherna COMMAND -h' or 'etherna COMMAND --help' for more information on a command.
+```
+
+# Issue reports
If you've discovered a bug, or have an idea for a new feature, please report it to our issue manager based on Jira https://etherna.atlassian.net/projects/EGC.
-## Questions? Problems?
+# Questions? Problems?
For questions or problems please write an email to [info@etherna.io](mailto:info@etherna.io).
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..dd4edcc
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/EthernaGatewayCli/Commands/Etherna/DownloadCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/DownloadCommand.cs
new file mode 100644
index 0000000..7863c41
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/DownloadCommand.cs
@@ -0,0 +1,54 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.BeeNet.Clients.GatewayApi;
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class DownloadCommand : CommandBase
+ {
+ // Fields.
+ private readonly IAuthenticationService authService;
+ private readonly IBeeGatewayClient beeGatewayClient;
+
+ // Constructor.
+ public DownloadCommand(
+ IAuthenticationService authService,
+ IBeeGatewayClient beeGatewayClient,
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ this.authService = authService;
+ this.beeGatewayClient = beeGatewayClient;
+ }
+
+ // Properties.
+ public override string CommandArgsHelpString => "RESOURCE";
+ public override string Description => "Download a resource from Swarm";
+
+ // Protected methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ if (!Options.RunAnonymously)
+ await authService.SignInAsync();
+
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/DownloadCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/DownloadCommandOptions.cs
new file mode 100644
index 0000000..e8a9a10
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/DownloadCommandOptions.cs
@@ -0,0 +1,34 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using System;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class DownloadCommandOptions : CommandOptionsBase
+ {
+ // Definitions.
+ public override IEnumerable Definitions => new CommandOption[]
+ {
+ new("-a", "--anon", "Download resource anonymously", _ => RunAnonymously = true),
+ new("-o", "--output", "Resource output path. Default: current directory", args => OutputPath = args[0], new[] { typeof(string) })
+ };
+
+ // Options.
+ public string OutputPath { get; private set; } = Environment.CurrentDirectory;
+ public bool RunAnonymously { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs
new file mode 100644
index 0000000..1ca39ba
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommand.cs
@@ -0,0 +1,66 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Postage
+{
+ public class CreateCommand : CommandBase
+ {
+ // Fields.
+ private readonly IAuthenticationService authService;
+ private readonly IGatewayService gatewayService;
+
+ // Constructor.
+ public CreateCommand(
+ IAuthenticationService authService,
+ IGatewayService gatewayService,
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ this.authService = authService;
+ this.gatewayService = gatewayService;
+ }
+
+ // Properties.
+ public override string Description => "Create a new postage batch";
+
+ // Methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ // Parse args.
+ if (commandArgs.Length != 0)
+ throw new ArgumentException("Create postage batch doesn't receive arguments");
+
+ // Authenticate user.
+ await authService.SignInAsync();
+
+ // Create postage.
+ var amount = Options.Amount ?? (Options.Ttl.HasValue
+ ? await gatewayService.CalculateAmountAsync(Options.Ttl.Value)
+ : throw new InvalidOperationException("Amount ot ttl are required"));
+ var batchId = await gatewayService.CreatePostageBatchAsync(amount, Options.Depth, Options.Label);
+
+ // Print result.
+ IoService.WriteLine();
+ IoService.WriteLine($"Postage batch id: {batchId}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommandOptions.cs
new file mode 100644
index 0000000..5135dc5
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Postage/CreateCommandOptions.cs
@@ -0,0 +1,47 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Models.Commands.OptionRequirements;
+using System;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Postage
+{
+ public class CreateCommandOptions : CommandOptionsBase
+ {
+ // Definitions.
+ public override IEnumerable Definitions => new CommandOption[]
+ {
+ new("-a", "--amount", "Specify the amount to use", args => Amount = long.Parse(args[0]), [typeof(long)]),
+ new("-d", "--depth", "Specify the postage batch depth", args => Depth = int.Parse(args[0]), [typeof(int)]),
+ new("-l", "--label", "Set a custom postage batch label", args => Label = args[0], [typeof(string)]),
+ new("-t", "--ttl", "Specify the time to live to obtain in days", args => Ttl = TimeSpan.FromDays(int.Parse(args[0])), [typeof(int)])
+ };
+ public override IEnumerable Requirements => new OptionRequirementBase[]
+ {
+ new ExclusiveOptionRequirement("--amount", "--ttl"),
+ new RequireOneOfOptionRequirement("--amount", "--ttl"),
+ new RequireOneOfOptionRequirement("--depth"),
+ new MinValueOptionRequirement("--depth", 17),
+ new MinValueOptionRequirement("--ttl", 1)
+ };
+
+ // Options.
+ public long? Amount { get; private set; }
+ public int Depth { get; private set; }
+ public string? Label { get; private set; }
+ public TimeSpan? Ttl { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs
new file mode 100644
index 0000000..b3a76a6
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Postage/InfoCommand.cs
@@ -0,0 +1,77 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using Etherna.Sdk.GeneratedClients.Gateway;
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Postage
+{
+ public class InfoCommand : CommandBase
+ {
+ // Consts.
+ private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true };
+
+ // Fields.
+ private readonly IAuthenticationService authService;
+ private readonly IGatewayService gatewayService;
+
+ // Constructor.
+ public InfoCommand(
+ IAuthenticationService authService,
+ IGatewayService gatewayService,
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ this.authService = authService;
+ this.gatewayService = gatewayService;
+ }
+
+ // Properties.
+ public override string Description => "Get info about a postage batch";
+ public override string CommandArgsHelpString => "POSTAGE_ID";
+
+ // Methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ // Parse args.
+ if (commandArgs.Length != 1)
+ throw new ArgumentException("Get postage batch info requires exactly 1 argument");
+
+ // Authenticate user.
+ await authService.SignInAsync();
+
+ // Get postage info
+ PostageBatchDto? postageBatch = null;
+ try
+ {
+ postageBatch = await gatewayService.GetPostageBatchInfoAsync(commandArgs[0]);
+ }
+ catch (EthernaGatewayApiException e) when (e.StatusCode == 404)
+ { }
+
+ // Print result.
+ IoService.WriteLine();
+ IoService.WriteLine(postageBatch is null
+ ? "Postage batch not found."
+ : JsonSerializer.Serialize(postageBatch, SerializerOptions));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/PostageCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/PostageCommand.cs
new file mode 100644
index 0000000..2ec645a
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/PostageCommand.cs
@@ -0,0 +1,33 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class PostageCommand : CommandBase
+ {
+ // Constructor.
+ public PostageCommand(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ { }
+
+ // Properties.
+ public override string Description => "Manage postage batches";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommand.cs
new file mode 100644
index 0000000..a0b5b59
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommand.cs
@@ -0,0 +1,39 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Resource
+{
+ public class DefundCommand : CommandBase
+ {
+ public DefundCommand(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ }
+
+ public override string CommandArgsHelpString => "RESOURCE_ID";
+ public override string Description => "Defund resource budget";
+
+ protected override Task ExecuteAsync(string[] commandArgs)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommandOptions.cs
new file mode 100644
index 0000000..6f46f5f
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/DefundCommandOptions.cs
@@ -0,0 +1,37 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Models.Commands.OptionRequirements;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Resource
+{
+ public class DefundCommandOptions : CommandOptionsBase
+ {
+ public override IEnumerable Definitions => new[]
+ {
+ new CommandOption("-p", "--pin", "Defund resource pinning on gateway", _ => DefundPinning = true),
+ new CommandOption("-t", "--traffic", "Defund resource traffic to everyone", _ => DefundTraffic = true)
+ };
+
+ public override IEnumerable Requirements => new[]
+ {
+ new RequireOneOfOptionRequirement("-p", "-t")
+ };
+
+ public bool DefundPinning { get; set; }
+ public bool DefundTraffic { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs
new file mode 100644
index 0000000..4ab519e
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommand.cs
@@ -0,0 +1,92 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Resource
+{
+ public class FundCommand : CommandBase
+ {
+ // Fields.
+ private readonly IAuthenticationService authService;
+ private readonly IGatewayService gatewayService;
+
+ // Constructor.
+ public FundCommand(
+ IAuthenticationService authService,
+ IGatewayService gatewayService,
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ this.authService = authService;
+ this.gatewayService = gatewayService;
+ }
+
+ // Properties.
+ public override string CommandArgsHelpString => "RESOURCE_ID";
+ public override string Description => "Fund resource budget";
+
+ // Methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ // Parse args.
+ if (commandArgs.Length != 1)
+ throw new ArgumentException("Fund resource require exactly 1 argument");
+ var resourceHash = commandArgs[0];
+
+ // Authenticate user.
+ await authService.SignInAsync();
+
+ // Fund resource.
+ IoService.WriteLine($"Funding resource {resourceHash}...");
+ if (Options.FundPinning)
+ {
+#pragma warning disable CA1031
+ try
+ {
+ await gatewayService.FundResourcePinningAsync(resourceHash);
+ IoService.WriteLine($"Resource pinning funded");
+ }
+ catch (Exception e)
+ {
+ IoService.WriteErrorLine($"Error funding resource pinning");
+ IoService.WriteLine(e.ToString());
+ }
+#pragma warning restore CA1031
+ }
+
+ if (Options.FundTraffic)
+ {
+#pragma warning disable CA1031
+ try
+ {
+ await gatewayService.FundResourceTrafficAsync(resourceHash);
+ IoService.WriteLine($"Resource traffic funded");
+ }
+ catch (Exception e)
+ {
+ IoService.WriteErrorLine($"Error funding resource traffic");
+ IoService.WriteLine(e.ToString());
+ }
+#pragma warning restore CA1031
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommandOptions.cs
new file mode 100644
index 0000000..8a21402
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/FundCommandOptions.cs
@@ -0,0 +1,37 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Models.Commands.OptionRequirements;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Resource
+{
+ public class FundCommandOptions : CommandOptionsBase
+ {
+ public override IEnumerable Definitions => new[]
+ {
+ new CommandOption("-p", "--pin", "Fund resource pinning on gateway", _ => FundPinning = true),
+ new CommandOption("-t", "--traffic", "Fund resource traffic to everyone", _ => FundTraffic = true)
+ };
+
+ public override IEnumerable Requirements => new[]
+ {
+ new RequireOneOfOptionRequirement("-p", "-t")
+ };
+
+ public bool FundPinning { get; set; }
+ public bool FundTraffic { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/Resource/ListCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/Resource/ListCommand.cs
new file mode 100644
index 0000000..6ee1f44
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/Resource/ListCommand.cs
@@ -0,0 +1,38 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna.Resource
+{
+ public class ListCommand : CommandBase
+ {
+ public ListCommand(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ }
+
+ public override string Description => "List resources";
+
+ protected override Task ExecuteAsync(string[] commandArgs)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/ResourceCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/ResourceCommand.cs
new file mode 100644
index 0000000..3e8a84b
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/ResourceCommand.cs
@@ -0,0 +1,33 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using System;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class ResourceCommand : CommandBase
+ {
+ // Constructor.
+ public ResourceCommand(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ { }
+
+ // Properties.
+ public override string Description => "Manage Swarm resources";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs b/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs
new file mode 100644
index 0000000..6bd7a5d
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/UploadCommand.cs
@@ -0,0 +1,215 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.BeeNet.InputModels;
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using Etherna.Sdk.GeneratedClients.Gateway;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class UploadCommand : CommandBase
+ {
+ // Consts.
+ private const int UploadMaxRetry = 10;
+ private readonly TimeSpan UploadRetryTimeSpan = TimeSpan.FromSeconds(5);
+
+ // Fields.
+ private readonly IAuthenticationService authService;
+ private readonly IFileService fileService;
+ private readonly IGatewayService gatewayService;
+
+ // Constructor.
+ public UploadCommand(
+ IAuthenticationService authService,
+ IFileService fileService,
+ IGatewayService gatewayService,
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ {
+ this.authService = authService;
+ this.fileService = fileService;
+ this.gatewayService = gatewayService;
+ }
+
+ // Properties.
+ public override string CommandArgsHelpString => "SOURCE_FILE [SOURCE_FILE ...]";
+ public override string Description => "Upload a file resource to Swarm";
+
+ // Methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ // Parse args.
+ if (commandArgs.Length < 1)
+ throw new ArgumentException("Upload requires 1 or more arguments");
+ var filePaths = commandArgs;
+
+ // Search files and calculate total file size.
+ var contentByteSize = 0L;
+ foreach (var filePath in filePaths)
+ {
+ if (!File.Exists(filePath))
+ throw new InvalidOperationException($"File {filePath} doesn't exist");
+
+ var fileInfo = new FileInfo(filePath);
+ contentByteSize += fileInfo.Length;
+ }
+
+ // Authenticate user.
+ await authService.SignInAsync();
+
+ // Identify postage batch to use.
+ var postageBatchId = await GetUsablePostageBatchIdAsync(contentByteSize);
+
+ // Upload file.
+ foreach (var filePath in filePaths)
+ {
+ IoService.WriteLine($"Uploading {filePath}...");
+
+ var uploadSucceeded = false;
+ string refHash = default!;
+ for (int i = 0; i < UploadMaxRetry && !uploadSucceeded; i++)
+ {
+ try
+ {
+ await using var fileStream = File.Open(filePath, FileMode.Open);
+ var mimeType = fileService.GetMimeType(filePath);
+
+ refHash = await gatewayService.UploadFileAsync(
+ postageBatchId,
+ fileStream,
+ Path.GetFileName(filePath),
+ mimeType,
+ Options.PinResource);
+ IoService.WriteLine($"Ref hash: {refHash}");
+
+ uploadSucceeded = true;
+ }
+#pragma warning disable CA1031
+ catch (Exception e)
+ {
+ IoService.WriteErrorLine($"Error uploading {filePath}");
+ IoService.WriteLine(e.ToString());
+
+ if (i + 1 < UploadMaxRetry)
+ {
+ Console.WriteLine("Retry...");
+ await Task.Delay(UploadRetryTimeSpan);
+ }
+ }
+#pragma warning restore CA1031
+ }
+
+ if (!uploadSucceeded)
+ IoService.WriteErrorLine($"Can't upload \"{filePath}\" after {UploadMaxRetry} retries");
+ else if (Options.OfferDownload)
+ {
+#pragma warning disable CA1031
+ try
+ {
+ await gatewayService.FundResourceTrafficAsync(refHash);
+ IoService.WriteLine($"Resource traffic funded");
+ }
+ catch (Exception e)
+ {
+ IoService.WriteErrorLine($"Error funding resource traffic");
+ IoService.WriteLine(e.ToString());
+ }
+#pragma warning restore CA1031
+ }
+ }
+ }
+
+ // Helpers.
+ private async Task GetUsablePostageBatchIdAsync(long contentByteSize)
+ {
+ if (Options.UsePostageBatchId is null)
+ {
+ //create a new postage batch
+ var batchDepth = gatewayService.CalculateDepth(contentByteSize);
+ var amount = await gatewayService.CalculateAmountAsync(Options.NewPostageTtl);
+ var bzzPrice = gatewayService.CalculateBzzPrice(amount, batchDepth);
+
+ IoService.WriteLine($"Required postage batch Depth: {batchDepth}, Amount: {amount}, BZZ price: {bzzPrice}");
+
+ if (!Options.NewPostageAutoPurchase)
+ {
+ bool validSelection = false;
+
+ while (validSelection == false)
+ {
+ IoService.WriteLine($"Confirm the batch purchase? Y to confirm, N to deny [Y|n]");
+
+ switch (IoService.ReadKey())
+ {
+ case { Key: ConsoleKey.Y }:
+ case { Key: ConsoleKey.Enter }:
+ validSelection = true;
+ break;
+ case { Key: ConsoleKey.N }:
+ throw new InvalidOperationException("Batch purchase denied");
+ default:
+ IoService.WriteLine("Invalid selection");
+ break;
+ }
+ }
+ }
+
+ //create batch
+ var postageBatchId = await gatewayService.CreatePostageBatchAsync(amount, batchDepth, Options.NewPostageLabel);
+
+ IoService.WriteLine($"Created postage batch: {postageBatchId}");
+
+ return postageBatchId;
+ }
+ else
+ {
+ //get info about existing postage batch
+ PostageBatchDto postageBatch;
+ try
+ {
+ postageBatch = await gatewayService.GetPostageBatchInfoAsync(Options.UsePostageBatchId);
+ }
+ catch (EthernaGatewayApiException e) when (e.StatusCode == 404)
+ {
+ IoService.WriteErrorLine($"Unable to find postage batch \"{Options.UsePostageBatchId}\".");
+ throw;
+ }
+
+ //verify if it is usable
+ if (!postageBatch.Usable)
+ {
+ IoService.WriteErrorLine($"Postage batch \"{Options.UsePostageBatchId}\" is not usable.");
+ throw new InvalidOperationException();
+ }
+
+ //verify if it has available space
+ if (gatewayService.CalculatePostageBatchByteSize(postageBatch) -
+ gatewayService.CalculateRequiredPostageBatchSpace(contentByteSize) < 0)
+ {
+ IoService.WriteErrorLine($"Postage batch \"{Options.UsePostageBatchId}\" has not enough space.");
+ throw new InvalidOperationException();
+ }
+
+ return postageBatch.Id;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/Etherna/UploadCommandOptions.cs b/src/EthernaGatewayCli/Commands/Etherna/UploadCommandOptions.cs
new file mode 100644
index 0000000..755ff78
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/Etherna/UploadCommandOptions.cs
@@ -0,0 +1,50 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Models.Commands.OptionRequirements;
+using System;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands.Etherna
+{
+ public class UploadCommandOptions : CommandOptionsBase
+ {
+ // Consts.
+ private static readonly TimeSpan DefaultPostageBatchTtl = TimeSpan.FromDays(365);
+
+ // Definitions.
+ public override IEnumerable Definitions => new CommandOption[]
+ {
+ new(null, "--postage", "Use an existing postage batch. Create a new otherwise", args => UsePostageBatchId = args[0], [typeof(string)]),
+ new("-A", "--auto-purchase", "Auto purchase new postage batch", _ => NewPostageAutoPurchase = true),
+ new("-l", "--label", "Label of new postage batch", args => NewPostageLabel = args[0], [typeof(string)]),
+ new("-t", "--ttl", $"TTL (days) of new postage batch (default: {DefaultPostageBatchTtl.Days} days)", args => NewPostageTtl = TimeSpan.FromDays(int.Parse(args[0])), [typeof(int)]),
+ new("-f", "--fund-traffic", "Fund resource traffic to everyone", _ => OfferDownload = true),
+ new(null, "--no-pin", "Don't pin resource (pinning enabled by default)", _ => PinResource = false)
+ };
+ public override IEnumerable Requirements => new OptionRequirementBase[]
+ {
+ new IfPresentThenOptionRequirement("--postage", new ForbiddenOptionRequirement("--auto-purchase", "--label", "--ttl"))
+ };
+
+ // Options.
+ public bool OfferDownload { get; private set; }
+ public bool NewPostageAutoPurchase { get; private set; }
+ public string? NewPostageLabel { get; private set; }
+ public TimeSpan NewPostageTtl { get; private set; } = DefaultPostageBatchTtl;
+ public bool PinResource { get; private set; } = true;
+ public string? UsePostageBatchId { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/EthernaCommand.cs b/src/EthernaGatewayCli/Commands/EthernaCommand.cs
new file mode 100644
index 0000000..1b10f88
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/EthernaCommand.cs
@@ -0,0 +1,49 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using Etherna.GatewayCli.Utilities;
+using System;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Commands
+{
+ public class EthernaCommand : CommandBase
+ {
+ // Constructor.
+ public EthernaCommand(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ { }
+
+ // Properties.
+ public override string Description => "A CLI interface to the Etherna Gateway";
+ public override bool IsRootCommand => true;
+
+ // Protected methods.
+ protected override async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ // Check for new versions.
+ var newVersionAvailable = await EthernaVersionControl.CheckNewVersionAsync(IoService);
+ if (newVersionAvailable && !Options.IgnoreUpdate)
+ return;
+
+ await ExecuteSubCommandAsync(commandArgs);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Commands/EthernaCommandOptions.cs b/src/EthernaGatewayCli/Commands/EthernaCommandOptions.cs
new file mode 100644
index 0000000..98b8315
--- /dev/null
+++ b/src/EthernaGatewayCli/Commands/EthernaCommandOptions.cs
@@ -0,0 +1,34 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands;
+using System;
+using System.Collections.Generic;
+
+namespace Etherna.GatewayCli.Commands
+{
+ public class EthernaCommandOptions : CommandOptionsBase
+ {
+ // Definitions.
+ public override IEnumerable Definitions => new CommandOption[]
+ {
+ new("-k", "--api-key", "Api Key (optional)", args => ApiKey = args[0], new[] { typeof(string) }),
+ new("-i", "--ignore-update", "Ignore new versions of EthernaGatewayCli", _ => IgnoreUpdate = true)
+ };
+
+ // Options.
+ public string? ApiKey { get; private set; }
+ public bool IgnoreUpdate{ get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/CommonConsts.cs b/src/EthernaGatewayCli/CommonConsts.cs
new file mode 100644
index 0000000..91b1324
--- /dev/null
+++ b/src/EthernaGatewayCli/CommonConsts.cs
@@ -0,0 +1,28 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.BeeNet.Clients.GatewayApi;
+using System;
+
+namespace Etherna.GatewayCli
+{
+ public static class CommonConsts
+ {
+ public const string EthernaGatewayCliClientId = "ethernaGatewayCliId";
+ public const string EthernaGatewayUrl = "https://gateway.etherna.io/";
+ public const string EthernaSsoUrl = "https://sso.etherna.io/";
+ public static readonly TimeSpan GnosisBlockTime = TimeSpan.FromSeconds(5);
+ public const string HttpClientName = "ethernaAuthnHttpClient";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/EthernaGatewayCli.csproj b/src/EthernaGatewayCli/EthernaGatewayCli.csproj
index 2f4fc77..0834a06 100644
--- a/src/EthernaGatewayCli/EthernaGatewayCli.csproj
+++ b/src/EthernaGatewayCli/EthernaGatewayCli.csproj
@@ -1,10 +1,47 @@
-
- Exe
- net8.0
- enable
- enable
-
+
+ Exe
+ net8.0
+ Etherna.GatewayCli
+
+ Etherna SA
+ A CLI interface to the Etherna Gateway
+
+ true
+ enable
+ true
+ AllEnabledByDefault
+
+ true
+
+ etherna
+ https://github.com/Etherna/etherna-gateway-cli
+ git
+ true
+ true
+ snupkg
+ LICENSE
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/src/EthernaGatewayCli/Models/Commands/CommandBase.cs b/src/EthernaGatewayCli/Models/Commands/CommandBase.cs
new file mode 100644
index 0000000..1d2c487
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/CommandBase.cs
@@ -0,0 +1,319 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Services;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Models.Commands
+{
+ public abstract class CommandBase
+ {
+ // Fields.
+ private readonly IServiceProvider serviceProvider;
+ private ImmutableArray? _availableSubCommandTypes;
+ private ImmutableArray? _commandPathTypes;
+
+ // Constructor.
+ protected CommandBase(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ {
+ IoService = ioService;
+ this.serviceProvider = serviceProvider;
+ }
+
+ // Properties.
+ public ImmutableArray AvailableSubCommandTypes
+ {
+ get
+ {
+ if (_availableSubCommandTypes is null)
+ {
+ var subCommandsNamespace = GetType().Namespace + "." + GetType().Name.Replace("Command", "");
+ _availableSubCommandTypes = typeof(Program).GetTypeInfo().Assembly.GetTypes()
+ .Where(t => t is {IsClass:true, IsAbstract: false} &&
+ t.Namespace == subCommandsNamespace &&
+ typeof(CommandBase).IsAssignableFrom(t))
+ .OrderBy(t => t.Name)
+ .ToImmutableArray();
+ }
+ return _availableSubCommandTypes.Value;
+ }
+ }
+ public string CommandPathNames => string.Join(' ',
+ CommandPathTypes.Select(cType => ((CommandBase)serviceProvider.GetRequiredService(cType)).Name));
+ public ImmutableArray CommandPathTypes
+ {
+ get
+ {
+ if (_commandPathTypes is null)
+ {
+ var currentCommandNamespace = GetType().Namespace;
+ if (currentCommandNamespace is null)
+ throw new InvalidOperationException();
+
+ _commandPathTypes = GetParentCommandTypesFromNamespace(currentCommandNamespace)
+ .Append(GetType()).ToImmutableArray();
+ }
+ return _commandPathTypes.Value;
+ }
+ }
+ public virtual string CommandArgsHelpString => HasSubCommands ? "COMMAND" : "";
+ public string CommandPathUsageHelpString
+ {
+ get
+ {
+ var strBuilder = new StringBuilder();
+ foreach (var commandType in CommandPathTypes)
+ {
+ var command = (CommandBase)serviceProvider.GetRequiredService(commandType);
+ strBuilder.Append(command.Name);
+ if (command.HasOptions)
+ {
+ strBuilder.Append(command.HasRequiredOptions ?
+ $" {command.Name.ToUpperInvariant()}_OPTIONS" :
+ $" [{command.Name.ToUpperInvariant()}_OPTIONS]");
+ }
+
+ strBuilder.Append(' ');
+ }
+ strBuilder.Append(CommandArgsHelpString);
+ return strBuilder.ToString();
+ }
+ }
+ public abstract string Description { get; }
+ public virtual bool HasOptions => false;
+ public virtual bool HasRequiredOptions => false;
+ public bool HasSubCommands => AvailableSubCommandTypes.Any();
+ public virtual bool IsRootCommand => false;
+ public string Name => GetCommandNameFromType(GetType());
+ public virtual bool PrintHelpWithNoArgs => true;
+
+ // Protected properties.
+ protected IIoService IoService { get; }
+
+ // Public methods.
+ public async Task RunAsync(string[] args)
+ {
+ // Parse arguments.
+ var printHelp = EvaluatePrintHelp(args);
+ var optionArgsCount = printHelp ? 0 : ParseOptionArgs(args);
+
+ // Print help or run command.
+ if (printHelp)
+ PrintHelp();
+ else
+ await ExecuteAsync(args[optionArgsCount..]);
+ }
+
+ // Protected methods.
+ protected virtual void AppendOptionsHelp(StringBuilder strBuilder) { }
+
+ ///
+ /// Parse command options
+ ///
+ /// Input args
+ /// Found option args counter
+ protected virtual int ParseOptionArgs(string[] args) => 0;
+
+ protected virtual async Task ExecuteAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+ await ExecuteSubCommandAsync(commandArgs);
+ }
+
+ protected async Task ExecuteSubCommandAsync(string[] commandArgs)
+ {
+ ArgumentNullException.ThrowIfNull(commandArgs, nameof(commandArgs));
+
+ if (commandArgs.Length == 0)
+ throw new ArgumentException("A command name is required");
+
+ var subCommandName = commandArgs[0];
+ var subCommandArgs = commandArgs[1..];
+
+ var selectedCommandType = AvailableSubCommandTypes.FirstOrDefault(
+ t => GetCommandNameFromType(t) == subCommandName);
+
+ if (selectedCommandType is null)
+ throw new ArgumentException($"{CommandPathNames}: '{subCommandName}' is not a valid command.");
+
+ var selectedCommand = (CommandBase)serviceProvider.GetRequiredService(selectedCommandType);
+ await selectedCommand.RunAsync(subCommandArgs);
+ }
+
+ // Protected helpers.
+ protected static string GetCommandNameFromType(Type commandType)
+ {
+ ArgumentNullException.ThrowIfNull(commandType, nameof(commandType));
+
+ if (!typeof(CommandBase).IsAssignableFrom(commandType))
+ throw new ArgumentException($"{commandType.Name} is not a command type");
+
+ return commandType.Name.Replace("Command", "").ToLowerInvariant();
+ }
+
+ // Private helpers.
+ private bool EvaluatePrintHelp(string[] args)
+ {
+ ArgumentNullException.ThrowIfNull(args, nameof(args));
+
+ switch (args.Length)
+ {
+ case 0 when PrintHelpWithNoArgs:
+ return true;
+ case 1:
+ switch (args[0])
+ {
+ case "-h":
+ case "--help":
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ private static IEnumerable GetParentCommandTypesFromNamespace(string currentNamespace)
+ {
+ var lastSeparatorIndex = currentNamespace.LastIndexOf('.');
+ var parentNamespace = currentNamespace[..lastSeparatorIndex];
+ var parentCommandName = currentNamespace[(lastSeparatorIndex + 1)..] + "Command";
+ var parentCommandType = typeof(CommandBase).GetTypeInfo().Assembly.GetTypes()
+ .FirstOrDefault(t => t is { IsClass: true, IsAbstract: false } &&
+ t.FullName == parentNamespace + '.' + parentCommandName &&
+ typeof(CommandBase).IsAssignableFrom(t));
+
+ if (parentCommandType is null)
+ return Array.Empty();
+ return GetParentCommandTypesFromNamespace(parentNamespace).Append(parentCommandType);
+ }
+
+ [SuppressMessage("Performance", "CA1851:Possible multiple enumerations of \'IEnumerable\' collection")]
+ private void PrintHelp()
+ {
+ var strBuilder = new StringBuilder();
+
+ // Add name and description.
+ strBuilder.AppendLine(CommandPathNames);
+ strBuilder.AppendLine(Description);
+ strBuilder.AppendLine();
+
+ // Add usage.
+ strBuilder.AppendLine($"Usage: {CommandPathUsageHelpString}");
+ strBuilder.AppendLine();
+
+ // Add sub commands.
+ var availableSubCommandTypes = AvailableSubCommandTypes;
+ if (availableSubCommandTypes.Any())
+ {
+ var allSubCommands = availableSubCommandTypes.Select(t => (CommandBase)serviceProvider.GetRequiredService(t));
+
+ strBuilder.AppendLine("Commands:");
+ var descriptionShift = allSubCommands.Select(c => c.Name.Length).Max() + 4;
+ foreach (var command in allSubCommands)
+ {
+ strBuilder.Append(" ");
+ strBuilder.Append(command.Name);
+ for (int i = 0; i < descriptionShift - command.Name.Length; i++)
+ strBuilder.Append(' ');
+ strBuilder.AppendLine(command.Description);
+ }
+ strBuilder.AppendLine();
+ }
+
+ // Add options.
+ AppendOptionsHelp(strBuilder);
+
+ // Add print help.
+ strBuilder.AppendLine($"Run '{CommandPathNames} -h' or '{CommandPathNames} --help' to print help.");
+ if (IsRootCommand)
+ strBuilder.AppendLine($"Run '{CommandPathNames} COMMAND -h' or '{CommandPathNames} COMMAND --help' for more information on a command.");
+ strBuilder.AppendLine();
+
+ // Print it.
+ var helpOutput = strBuilder.ToString();
+ IoService.Write(helpOutput);
+ }
+ }
+
+ public abstract class CommandBase : CommandBase
+ where TOptions: CommandOptionsBase, new()
+ {
+ // Constructor.
+ protected CommandBase(
+ IIoService ioService,
+ IServiceProvider serviceProvider)
+ : base(ioService, serviceProvider)
+ { }
+
+ // Properties.
+ public override bool HasOptions => true;
+ public override bool HasRequiredOptions => Options.AreRequired;
+ public TOptions Options { get; } = new TOptions();
+
+ // Methods.
+ protected override int ParseOptionArgs(string[] args) => Options.ParseArgs(args, IoService);
+
+ protected override void AppendOptionsHelp(StringBuilder strBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(strBuilder, nameof(strBuilder));
+
+ if (!Options.Definitions.Any()) return;
+
+ // Option descriptions.
+ strBuilder.AppendLine("Options:");
+ var descriptionShift = Options.Definitions.Select(opt =>
+ {
+ var len = opt.LongName.Length;
+ foreach (var reqArgType in opt.RequiredArgTypes)
+ len += reqArgType.Name.Length + 1;
+ return len;
+ }).Max() + 4;
+ foreach (var option in Options.Definitions)
+ {
+ strBuilder.Append(" ");
+ strBuilder.Append(option.ShortName is null ? " " : $"{option.ShortName}, ");
+ strBuilder.Append(option.LongName);
+ var strLen = option.LongName.Length;
+ foreach (var reqArgType in option.RequiredArgTypes)
+ {
+ strBuilder.Append($" {reqArgType.Name.ToLower()}");
+ strLen += reqArgType.Name.Length + 1;
+ }
+ for (int i = 0; i < descriptionShift - strLen; i++)
+ strBuilder.Append(' ');
+ strBuilder.AppendLine(option.Description);
+ }
+ strBuilder.AppendLine();
+
+ // Requirements.
+ if (Options.Requirements.Any())
+ {
+ strBuilder.AppendLine("Option requirements:");
+ foreach (var requirement in Options.Requirements)
+ strBuilder.AppendLine(" " + requirement.PrintHelpLine(Options));
+ strBuilder.AppendLine();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/CommandOption.cs b/src/EthernaGatewayCli/Models/Commands/CommandOption.cs
new file mode 100644
index 0000000..a00a3fb
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/CommandOption.cs
@@ -0,0 +1,55 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Etherna.GatewayCli.Models.Commands
+{
+ public class CommandOption
+ {
+ // Regex to validate option names.
+ private static readonly Regex shortNameRegex = new("^-[A-Za-z0-9]$");
+ private static readonly Regex longNameRegex = new("^--[A-Za-z0-9-]+$");
+
+ // Constructor.
+ public CommandOption(
+ string? shortName,
+ string longName,
+ string description,
+ Action onFound,
+ IEnumerable? requiredArgTypes = null)
+ {
+ if (shortName != null && !shortNameRegex.IsMatch(shortName))
+ throw new ArgumentException("Invalid short option name");
+
+ if (!longNameRegex.IsMatch(longName))
+ throw new ArgumentException("Invalid long option name");
+
+ ShortName = shortName;
+ LongName = longName;
+ RequiredArgTypes = requiredArgTypes ?? Array.Empty();
+ Description = description;
+ OnFound = onFound;
+ }
+
+ // Properties.
+ public string Description { get; }
+ public Action OnFound { get; }
+ public string LongName { get; }
+ public IEnumerable RequiredArgTypes { get; }
+ public string? ShortName { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/CommandOptionsBase.cs b/src/EthernaGatewayCli/Models/Commands/CommandOptionsBase.cs
new file mode 100644
index 0000000..55cf79d
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/CommandOptionsBase.cs
@@ -0,0 +1,93 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Commands.OptionRequirements;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Etherna.GatewayCli.Models.Commands
+{
+ public abstract class CommandOptionsBase
+ {
+ // Properties.
+ public bool AreRequired => Requirements.OfType().Any();
+ public abstract IEnumerable Definitions { get; }
+ public virtual IEnumerable Requirements => Array.Empty();
+
+ // Methods.
+ public CommandOption FindOptionByName(string name) =>
+ Definitions.First(o => o.ShortName == name || o.LongName == name);
+
+ ///
+ /// Parse command options
+ ///
+ /// Input args
+ /// Found option args counter
+ public virtual int ParseArgs(
+ string[] args,
+ IIoService ioService)
+ {
+ ArgumentNullException.ThrowIfNull(args, nameof(args));
+ ArgumentNullException.ThrowIfNull(ioService, nameof(args));
+
+ // Parse options.
+ var parsedArgsCount = 0;
+ var foundOptions = new List();
+ while (parsedArgsCount < args.Length && args[parsedArgsCount].StartsWith('-'))
+ {
+ var optName = args[parsedArgsCount++];
+
+ // Find option by name.
+ var foundOption = Definitions.FirstOrDefault(opt => opt.ShortName == optName || opt.LongName == optName);
+ if (foundOption is null)
+ throw new ArgumentException(optName + " is not a valid option");
+
+ // Verify duplicate options.
+ if (foundOptions.Any(opt => opt.Option.ShortName == optName || opt.Option.LongName == optName))
+ throw new ArgumentException(optName + " option is duplicate");
+
+ // Check required option args.
+ if (args.Length - parsedArgsCount < foundOption.RequiredArgTypes.Count())
+ throw new ArgumentException($"{optName} requires {foundOption.RequiredArgTypes.Count()} args: {string.Join(" ", foundOption.RequiredArgTypes.Select(t => t.Name.ToLower()))}");
+
+ // Exec option code.
+ var requiredOptArgs = args[parsedArgsCount..(parsedArgsCount + foundOption.RequiredArgTypes.Count())];
+ parsedArgsCount += requiredOptArgs.Length;
+ foundOption.OnFound(requiredOptArgs);
+
+ // Save on found options.
+ foundOptions.Add(new(foundOption, optName, requiredOptArgs));
+ }
+
+ // Verify option requirements.
+ var optionErrors = Requirements.SelectMany(r => r.ValidateOptions(this, foundOptions)).ToArray();
+ if (optionErrors.Length != 0)
+ {
+ var errorStrBuilder = new StringBuilder();
+ errorStrBuilder.AppendLine("Invalid options:");
+ foreach (var error in optionErrors)
+ errorStrBuilder.AppendLine(" " + error.Message);
+
+ ioService.WriteError(errorStrBuilder.ToString());
+
+ throw new ArgumentException("Errors with command options");
+ }
+
+ return parsedArgsCount;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ExclusiveOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ExclusiveOptionRequirement.cs
new file mode 100644
index 0000000..f4de0a9
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ExclusiveOptionRequirement.cs
@@ -0,0 +1,49 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class ExclusiveOptionRequirement(params string[] optionsNames)
+ : OptionRequirementBase(optionsNames)
+ {
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions) =>
+ ComposeSentence(OptionsNames.Select(n => commandOptions.FindOptionByName(n).LongName));
+
+ public override IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions)
+ {
+ if (OptionsNames.Count(optName => TryFindParsedOption(parsedOptions, optName, out _)) >= 2)
+ {
+ var invalidParsedNames = parsedOptions.Where(parsedOpt =>
+ OptionsNames.Contains(parsedOpt.Option.ShortName) ||
+ OptionsNames.Contains(parsedOpt.Option.LongName))
+ .Select(foundOpt => foundOpt.ParsedName);
+
+ return [new OptionRequirementError(ComposeSentence(invalidParsedNames))];
+ }
+
+ return Array.Empty();
+ }
+
+ // Private helpers.
+ private string ComposeSentence(IEnumerable optNames) =>
+ $"{string.Join(", ", optNames)} are mutual exclusive.";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ForbiddenOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ForbiddenOptionRequirement.cs
new file mode 100644
index 0000000..9067b25
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/ForbiddenOptionRequirement.cs
@@ -0,0 +1,48 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class ForbiddenOptionRequirement(params string[] optionsNames)
+ : OptionRequirementBase(optionsNames)
+ {
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions) =>
+ string.Join(", ", OptionsNames.Select(n => commandOptions.FindOptionByName(n).LongName)) +
+ (OptionsNames.Count == 1 ? " is forbidden." : " are forbidden.");
+
+ public override IEnumerable ValidateOptions(CommandOptionsBase commandOptions, IEnumerable parsedOptions)
+ {
+ if (OptionsNames.Any(optName => TryFindParsedOption(parsedOptions, optName, out _)))
+ {
+ var invalidParsedNames = parsedOptions.Where(parsedOpt =>
+ OptionsNames.Contains(parsedOpt.Option.ShortName) ||
+ OptionsNames.Contains(parsedOpt.Option.LongName))
+ .Select(foundOpt => foundOpt.ParsedName);
+
+ return [new OptionRequirementError(ComposeSentence(invalidParsedNames))];
+ }
+
+ return Array.Empty();
+ }
+
+ // Private helpers.
+ private string ComposeSentence(IEnumerable optNames) =>
+ string.Join(", ", optNames) + (optNames.Count() == 1 ? " is forbidden." : " are forbidden.");
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/IfPresentThenOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/IfPresentThenOptionRequirement.cs
new file mode 100644
index 0000000..2fa9637
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/IfPresentThenOptionRequirement.cs
@@ -0,0 +1,54 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class IfPresentThenOptionRequirement(
+ string optionsName,
+ OptionRequirementBase thenRequirement)
+ : OptionRequirementBase([optionsName])
+ {
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions)
+ {
+ ArgumentNullException.ThrowIfNull(commandOptions, nameof(commandOptions));
+
+ return ComposeSentence(
+ commandOptions.FindOptionByName(OptionsNames.First()).LongName,
+ thenRequirement.PrintHelpLine(commandOptions),
+ commandOptions);
+ }
+
+ public override IEnumerable ValidateOptions(CommandOptionsBase commandOptions, IEnumerable parsedOptions)
+ {
+ var optName = OptionsNames.First();
+
+ if (!TryFindParsedOption(parsedOptions, optName, out var parsedOption))
+ return Array.Empty();
+
+ var thenErrors = thenRequirement.ValidateOptions(commandOptions, parsedOptions);
+
+ return thenErrors.Select(thenError =>
+ new OptionRequirementError(ComposeSentence(optName, thenError.Message, commandOptions)));
+ }
+
+ // Private helpers.
+ private string ComposeSentence(string optName, string thenMessageLine, CommandOptionsBase commandOptions) =>
+ $"If {optName} is present then {thenMessageLine}";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MaxValueOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MaxValueOptionRequirement.cs
new file mode 100644
index 0000000..2e60c5f
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MaxValueOptionRequirement.cs
@@ -0,0 +1,55 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class MaxValueOptionRequirement(
+ string optionsName,
+ double maxValue)
+ : OptionRequirementBase([optionsName])
+ {
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions)
+ {
+ ArgumentNullException.ThrowIfNull(commandOptions, nameof(commandOptions));
+
+ return ComposeSentence(commandOptions.FindOptionByName(OptionsNames.First()).LongName);
+ }
+
+ public override IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions)
+ {
+ var optName = OptionsNames.First();
+
+ if (!TryFindParsedOption(parsedOptions, optName, out var parsedOption))
+ return Array.Empty();
+
+ if (!double.TryParse(parsedOption!.ParsedArgs.First(), out var doubleArg))
+ return [new OptionRequirementError(
+ $"Invalid argument value: {parsedOption.ParsedName} {parsedOption.ParsedArgs.First()}")];
+
+ return doubleArg <= maxValue
+ ? Array.Empty()
+ : [new OptionRequirementError(ComposeSentence(parsedOption.ParsedName))];
+ }
+
+ // Private helpers.
+ private string ComposeSentence(string optName) => $"{optName} has max value {maxValue}.";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MinValueOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MinValueOptionRequirement.cs
new file mode 100644
index 0000000..b0479c1
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/MinValueOptionRequirement.cs
@@ -0,0 +1,55 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class MinValueOptionRequirement(
+ string optionsName,
+ double minValue)
+ : OptionRequirementBase([optionsName])
+ {
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions)
+ {
+ ArgumentNullException.ThrowIfNull(commandOptions, nameof(commandOptions));
+
+ return ComposeSentence(commandOptions.FindOptionByName(OptionsNames.First()).LongName);
+ }
+
+ public override IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions)
+ {
+ var optName = OptionsNames.First();
+
+ if (!TryFindParsedOption(parsedOptions, optName, out var parsedOption))
+ return Array.Empty();
+
+ if (!double.TryParse(parsedOption!.ParsedArgs.First(), out var doubleArg))
+ return [new OptionRequirementError(
+ $"Invalid argument value: {parsedOption.ParsedName} {parsedOption.ParsedArgs.First()}")];
+
+ return doubleArg >= minValue
+ ? Array.Empty()
+ : [new OptionRequirementError(ComposeSentence(parsedOption.ParsedName))];
+ }
+
+ // Private helpers.
+ private string ComposeSentence(string optName) => $"{optName} has min value {minValue}.";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementBase.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementBase.cs
new file mode 100644
index 0000000..e4afe9d
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementBase.cs
@@ -0,0 +1,46 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public abstract class OptionRequirementBase(
+ IReadOnlyCollection optionsNames)
+ {
+ // Properties.
+ public IReadOnlyCollection OptionsNames { get; protected set; } = optionsNames;
+
+ // Methods.
+ public abstract string PrintHelpLine(
+ CommandOptionsBase commandOptions);
+
+ public abstract IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions);
+
+ // Protected helpers.
+ protected static bool TryFindParsedOption(
+ IEnumerable parsedOptions,
+ string optionName,
+ out ParsedOption? foundParsedOption)
+ {
+ foundParsedOption = parsedOptions.SingleOrDefault(parsOpt =>
+ parsOpt.Option.ShortName == optionName ||
+ parsOpt.Option.LongName == optionName);
+ return foundParsedOption != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementError.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementError.cs
new file mode 100644
index 0000000..d8c7405
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/OptionRequirementError.cs
@@ -0,0 +1,28 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class OptionRequirementError
+ {
+ // Constructor.
+ public OptionRequirementError(string message)
+ {
+ Message = message;
+ }
+
+ // Properties.
+ public string Message { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RangeOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RangeOptionRequirement.cs
new file mode 100644
index 0000000..747a453
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RangeOptionRequirement.cs
@@ -0,0 +1,68 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class RangeOptionRequirement : OptionRequirementBase
+ {
+ // Constructor.
+ public RangeOptionRequirement(string optionsName,
+ double minValue,
+ double maxValue) : base([optionsName])
+ {
+ if (minValue >= maxValue)
+ throw new ArgumentException("Min value must be smaller than max value");
+
+ MaxValue = maxValue;
+ MinValue = minValue;
+ }
+
+ // Properties.
+ public double MaxValue { get; }
+ public double MinValue { get; }
+
+ // Methods.
+ public override string PrintHelpLine(CommandOptionsBase commandOptions)
+ {
+ ArgumentNullException.ThrowIfNull(commandOptions, nameof(commandOptions));
+
+ return ComposeSentence(commandOptions.FindOptionByName(OptionsNames.First()).LongName);
+ }
+
+ public override IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions)
+ {
+ var optName = OptionsNames.First();
+
+ if (!TryFindParsedOption(parsedOptions, optName, out var parsedOption))
+ return Array.Empty();
+
+ if (!double.TryParse(parsedOption!.ParsedArgs.First(), out var doubleArg))
+ return [new OptionRequirementError(
+ $"Invalid argument value: {parsedOption.ParsedName} {parsedOption.ParsedArgs.First()}")];
+
+ return doubleArg >= MinValue && doubleArg <= MaxValue
+ ? Array.Empty()
+ : [new OptionRequirementError(ComposeSentence(parsedOption.ParsedName))];
+ }
+
+ // Private helpers.
+ private string ComposeSentence(string optName) => $"{optName} has value in range [{MinValue}, {MaxValue}].";
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RequireOneOfOptionRequirement.cs b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RequireOneOfOptionRequirement.cs
new file mode 100644
index 0000000..a481e10
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/OptionRequirements/RequireOneOfOptionRequirement.cs
@@ -0,0 +1,35 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Models.Commands.OptionRequirements
+{
+ public class RequireOneOfOptionRequirement(params string[] optionsNames)
+ : OptionRequirementBase(optionsNames)
+ {
+ public override string PrintHelpLine(CommandOptionsBase commandOptions) =>
+ string.Join(", ", OptionsNames.Select(n => commandOptions.FindOptionByName(n).LongName)) +
+ (OptionsNames.Count == 1 ? " is required." : " at least one is required.");
+
+ public override IEnumerable ValidateOptions(
+ CommandOptionsBase commandOptions,
+ IEnumerable parsedOptions) =>
+ OptionsNames.Any(optName => TryFindParsedOption(parsedOptions, optName, out _)) ?
+ Array.Empty() :
+ [new OptionRequirementError(PrintHelpLine(commandOptions))];
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Commands/ParsedOption.cs b/src/EthernaGatewayCli/Models/Commands/ParsedOption.cs
new file mode 100644
index 0000000..d11e562
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Commands/ParsedOption.cs
@@ -0,0 +1,30 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Etherna.GatewayCli.Models.Commands
+{
+ public class ParsedOption(
+ CommandOption option,
+ string parsedName,
+ params string[] parsedArgs)
+ {
+ // Properties.
+ public CommandOption Option { get; } = option;
+ public ReadOnlyCollection ParsedArgs { get; } = parsedArgs.AsReadOnly();
+ public string ParsedName { get; } = parsedName;
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/Domain/BzzBalance.cs b/src/EthernaGatewayCli/Models/Domain/BzzBalance.cs
new file mode 100644
index 0000000..d81d896
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/Domain/BzzBalance.cs
@@ -0,0 +1,89 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Globalization;
+
+namespace Etherna.GatewayCli.Models.Domain
+{
+ public struct BzzBalance : IEquatable
+ {
+ // Consts.
+ public const int DecimalPrecision = 16;
+
+ // Fields.
+ private readonly decimal _balance;
+
+ // Constructors.
+ public BzzBalance(decimal balance)
+ {
+ _balance = decimal.Round(balance, DecimalPrecision)
+ / 1.000000000000000000000000000000000m; //remove final zeros
+ }
+
+ // Methods.
+ public int CompareTo(BzzBalance other) => _balance.CompareTo(other._balance);
+
+ public override bool Equals(object? obj) =>
+ obj is BzzBalance xDaiObj &&
+ Equals(xDaiObj);
+
+ public bool Equals(BzzBalance other) => _balance == other._balance;
+ public BzzBalance FromDecimal(decimal value) => new(value);
+ public BzzBalance FromDouble(double value) => new((decimal)value);
+ public BzzBalance FromInt32(int value) => new(value);
+ public override int GetHashCode() => _balance.GetHashCode();
+ public decimal ToDecimal() => _balance;
+ public override string ToString() => _balance.ToString(CultureInfo.InvariantCulture);
+
+ // Static methods.
+ public static BzzBalance Add(BzzBalance left, BzzBalance right) => left + right;
+ public static BzzBalance Decrement(BzzBalance balance) => --balance;
+ public static BzzBalance Divide(BzzBalance left, BzzBalance right) => left / right;
+ public static BzzBalance Increment(BzzBalance balance) => ++balance;
+ public static BzzBalance Multiply(BzzBalance left, BzzBalance right) => left * right;
+ public static BzzBalance Negate(BzzBalance balance) => -balance;
+ public static BzzBalance Subtract(BzzBalance left, BzzBalance right) => left - right;
+
+ // Operator methods.
+ public static BzzBalance operator +(BzzBalance left, BzzBalance right) =>
+ new(left._balance + right._balance);
+
+ public static BzzBalance operator -(BzzBalance left, BzzBalance right) =>
+ new(left._balance - right._balance);
+
+ public static BzzBalance operator *(BzzBalance left, BzzBalance right) =>
+ new(left._balance * right._balance);
+
+ public static BzzBalance operator /(BzzBalance left, BzzBalance right) =>
+ new(left._balance / right._balance);
+
+ public static bool operator ==(BzzBalance left, BzzBalance right) => left.Equals(right);
+ public static bool operator !=(BzzBalance left, BzzBalance right) => !(left == right);
+ public static bool operator >(BzzBalance left, BzzBalance right) => left._balance > right._balance;
+ public static bool operator <(BzzBalance left, BzzBalance right) => left._balance < right._balance;
+ public static bool operator >=(BzzBalance left, BzzBalance right) => left._balance >= right._balance;
+ public static bool operator <=(BzzBalance left, BzzBalance right) => left._balance <= right._balance;
+ public static BzzBalance operator -(BzzBalance value) => new(-value._balance);
+ public static BzzBalance operator ++(BzzBalance value) => new(value._balance + 1);
+ public static BzzBalance operator --(BzzBalance value) => new(value._balance - 1);
+
+ // Implicit conversion operator methods.
+ public static implicit operator BzzBalance(decimal value) => new(value);
+ public static implicit operator BzzBalance(double value) => new((decimal)value);
+ public static implicit operator BzzBalance(int value) => new(value);
+
+ public static explicit operator decimal(BzzBalance value) => value.ToDecimal();
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Models/GitHubDto/GitReleaseVersionDto.cs b/src/EthernaGatewayCli/Models/GitHubDto/GitReleaseVersionDto.cs
new file mode 100644
index 0000000..4d97cfe
--- /dev/null
+++ b/src/EthernaGatewayCli/Models/GitHubDto/GitReleaseVersionDto.cs
@@ -0,0 +1,33 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+
+namespace Etherna.GatewayCli.Models.GitHubDto
+{
+ public class GitReleaseVersionDto
+ {
+ // Properties.
+ public string Assets_url { get; set; } = default!;
+ public DateTime Created_at { get; set; }
+ public bool Draft { get; set; }
+ public string Html_url { get; set; } = default!;
+ public int Id { get; set; } = default!;
+ public string Name { get; set; } = default!;
+ public bool Prerelease { get; set; }
+ public DateTime Published_at { get; set; }
+ public string Tag_name { get; set; } = default!;
+ public string Url { get; set; } = default!;
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Program.cs b/src/EthernaGatewayCli/Program.cs
index 989a6d2..b6b3aa3 100644
--- a/src/EthernaGatewayCli/Program.cs
+++ b/src/EthernaGatewayCli/Program.cs
@@ -1,9 +1,124 @@
-namespace EthernaGatewayCli;
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
-class Program
+using Etherna.GatewayCli.Commands;
+using Etherna.GatewayCli.Models.Commands;
+using Etherna.GatewayCli.Services;
+using Etherna.Sdk.Users.Native;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli
{
- static void Main(string[] args)
+ internal sealed class Program
{
- Console.WriteLine("Hello, World!");
+ // Consts.
+ private static readonly string[] ApiScopes = ["userApi.gateway"];
+ private const string CommandsNamespace = "Etherna.GatewayCli.Commands";
+
+ // Methods.
+ public static async Task Main(string[] args)
+ {
+ // Setup DI.
+ var services = new ServiceCollection();
+
+ //commands
+ var availableCommandTypes = typeof(Program).GetTypeInfo().Assembly.GetTypes()
+ .Where(t => t is { IsClass: true, IsAbstract: false } &&
+ t.Namespace?.StartsWith(CommandsNamespace) == true &&
+ typeof(CommandBase).IsAssignableFrom(t))
+ .OrderBy(t => t.Name);
+ foreach (var commandType in availableCommandTypes)
+ services.AddTransient(commandType);
+
+ //services
+ services.AddCoreServices();
+
+ /****
+ * WORKAROUND
+ * See: https://etherna.atlassian.net/browse/EAUTH-21
+ * We need to configure the authentication method before of create Service Provider.
+ * Because of this, we decided to run only option parsing upfront, and so instantiate the real command.
+ */
+ var ethernaCommandOptions = new EthernaCommandOptions();
+ var tmpIoService = new ConsoleIoService();
+#pragma warning disable CA1031
+ try
+ {
+ if (args.Length != 1 || (args[0] != "-h" && args[0] != "--help"))
+ ethernaCommandOptions.ParseArgs(args, tmpIoService);
+ }
+ catch (Exception e)
+ {
+ tmpIoService.WriteLine();
+ tmpIoService.WriteLine(e.ToString());
+ return;
+ }
+#pragma warning restore CA1031
+ /* END WORKAROUND
+ ****/
+
+ // Register etherna service clients.
+ IEthernaUserClientsBuilder ethernaClientsBuilder;
+ if (ethernaCommandOptions.ApiKey is null) //"code" grant flow
+ {
+ ethernaClientsBuilder = services.AddEthernaUserClientsWithCodeAuth(
+ CommonConsts.EthernaSsoUrl,
+ CommonConsts.EthernaGatewayCliClientId,
+ null,
+ 11430,
+ ApiScopes,
+ CommonConsts.HttpClientName,
+ c =>
+ {
+ c.Timeout = TimeSpan.FromMinutes(30);
+ });
+ }
+ else //"password" grant flow
+ {
+ ethernaClientsBuilder = services.AddEthernaUserClientsWithApiKeyAuth(
+ CommonConsts.EthernaSsoUrl,
+ ethernaCommandOptions.ApiKey,
+ ApiScopes,
+ CommonConsts.HttpClientName,
+ c =>
+ {
+ c.Timeout = TimeSpan.FromMinutes(30);
+ });
+ }
+ ethernaClientsBuilder.AddEthernaGatewayClient(new Uri(CommonConsts.EthernaGatewayUrl));
+
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Start etherna command.
+ var ethernaCommand = serviceProvider.GetRequiredService();
+ var ioService = serviceProvider.GetRequiredService();
+
+#pragma warning disable CA1031
+ try
+ {
+ await ethernaCommand.RunAsync(args);
+ }
+ catch (Exception e)
+ {
+ ioService.WriteLine();
+ ioService.WriteLine(e.ToString());
+ }
+#pragma warning restore CA1031
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/EthernaGatewayCli/ServiceCollectionExtensions.cs b/src/EthernaGatewayCli/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..5311d4d
--- /dev/null
+++ b/src/EthernaGatewayCli/ServiceCollectionExtensions.cs
@@ -0,0 +1,47 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.BeeNet;
+using Etherna.BeeNet.Clients.GatewayApi;
+using Etherna.GatewayCli.Services;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Net.Http;
+
+namespace Etherna.GatewayCli
+{
+ internal static class ServiceCollectionExtensions
+ {
+ public static void AddCoreServices(
+ this IServiceCollection services)
+ {
+ // Add transient services.
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ // Add singleton services.
+ //bee.net
+ services.AddSingleton((sp) =>
+ {
+ var httpClientFactory = sp.GetRequiredService();
+ return new BeeGatewayClient(
+ httpClientFactory.CreateClient(CommonConsts.HttpClientName),
+ new Uri(CommonConsts.EthernaGatewayUrl));
+ });
+ services.AddSingleton();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/AuthenticationService.cs b/src/EthernaGatewayCli/Services/AuthenticationService.cs
new file mode 100644
index 0000000..22f5217
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/AuthenticationService.cs
@@ -0,0 +1,65 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.Authentication;
+using Etherna.Authentication.Native;
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Services
+{
+ public class AuthenticationService : IAuthenticationService
+ {
+ // Fields.
+ private readonly IEthernaOpenIdConnectClient ethernaOpenIdConnectClient;
+ private readonly IEthernaSignInService ethernaSignInService;
+ private readonly IIoService ioService;
+
+ // Constructor.
+ public AuthenticationService(
+ IEthernaOpenIdConnectClient ethernaOpenIdConnectClient,
+ IEthernaSignInService ethernaSignInService,
+ IIoService ioService)
+ {
+ this.ethernaOpenIdConnectClient = ethernaOpenIdConnectClient;
+ this.ethernaSignInService = ethernaSignInService;
+ this.ioService = ioService;
+ }
+
+ // Methods.
+ public async Task SignInAsync()
+ {
+ try
+ {
+ await ethernaSignInService.SignInAsync();
+ }
+ catch (InvalidOperationException)
+ {
+ ioService.WriteErrorLine("Error during authentication.");
+ throw;
+ }
+ catch (Win32Exception)
+ {
+ ioService.WriteErrorLine("Error opening browser on local system. Try to authenticate with API key.");
+ throw;
+ }
+
+ // Get info from authenticated user.
+ var userName = await ethernaOpenIdConnectClient.GetUsernameAsync();
+
+ ioService.WriteLine($"User {userName} authenticated");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/ConsoleIoService.cs b/src/EthernaGatewayCli/Services/ConsoleIoService.cs
new file mode 100644
index 0000000..b818bb4
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/ConsoleIoService.cs
@@ -0,0 +1,47 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+
+namespace Etherna.GatewayCli.Services
+{
+ public class ConsoleIoService : IIoService
+ {
+ // Consts.
+ private const ConsoleColor ErrorForegroundColor = ConsoleColor.DarkRed;
+
+ // Methods.
+ public ConsoleKeyInfo ReadKey() => Console.ReadKey();
+
+ public string? ReadLine() => Console.ReadLine();
+
+ public void Write(string? value) => Console.Write(value);
+
+ public void WriteError(string value)
+ {
+ Console.ForegroundColor = ErrorForegroundColor;
+ Console.Write(value);
+ Console.ResetColor();
+ }
+
+ public void WriteErrorLine(string value)
+ {
+ Console.ForegroundColor = ErrorForegroundColor;
+ Console.WriteLine(value);
+ Console.ResetColor();
+ }
+
+ public void WriteLine(string? value = null) => Console.WriteLine(value);
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/FileService.cs b/src/EthernaGatewayCli/Services/FileService.cs
new file mode 100644
index 0000000..6e9df66
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/FileService.cs
@@ -0,0 +1,52 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Microsoft.AspNetCore.StaticFiles;
+using MimeDetective;
+using System.Linq;
+
+namespace Etherna.GatewayCli.Services
+{
+ public class FileService : IFileService
+ {
+ // Consts.
+ const string DefaultContentType = "application/octet-stream";
+
+ // Fields.
+ private readonly FileExtensionContentTypeProvider extensionMimeTypeProvider = new();
+ private readonly ContentInspector contentMimeTypeProvider = new ContentInspectorBuilder
+ {
+ Definitions = MimeDetective.Definitions.Default.All()
+ }.Build();
+
+ // Methods.
+ public string GetMimeType(string filePath)
+ {
+ var contentType = GetMimeTypeFromExtension(filePath);
+ return contentType == DefaultContentType ? GetMimeTypeFromContent(filePath) : contentType;
+ }
+
+ public string GetMimeTypeFromContent(string filePath)
+ {
+ var result = contentMimeTypeProvider.Inspect(filePath);
+ var mimeTypes = result.ByMimeType().Select(mtm => mtm.MimeType);
+ return mimeTypes.FirstOrDefault() ?? DefaultContentType;
+ }
+
+ public string GetMimeTypeFromExtension(string filePath) =>
+ extensionMimeTypeProvider.TryGetContentType(filePath, out string? contentType)
+ ? contentType
+ : DefaultContentType;
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/GatewayService.cs b/src/EthernaGatewayCli/Services/GatewayService.cs
new file mode 100644
index 0000000..753e241
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/GatewayService.cs
@@ -0,0 +1,176 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.BeeNet.Clients.GatewayApi;
+using Etherna.GatewayCli.Models.Domain;
+using Etherna.Sdk.GeneratedClients.Gateway;
+using Etherna.Sdk.Users;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Services
+{
+ public class GatewayService : IGatewayService
+ {
+ // Const.
+ private readonly TimeSpan BatchCheckTimeSpan = new(0, 0, 0, 5);
+ private readonly TimeSpan BatchCreationTimeout = new(0, 0, 10, 0);
+ private readonly TimeSpan BatchUsableTimeout = new(0, 0, 10, 0);
+ private readonly long BzzDecimalPlacesToUnit = (long)Math.Pow(10, 16);
+ private const int ChunkByteSize = 4096;
+ private const int MinBatchDepth = 17;
+
+ // Fields.
+ private readonly IBeeGatewayClient beeGatewayClient;
+ private readonly IEthernaUserGatewayClient ethernaGatewayClient;
+ private readonly IIoService ioService;
+
+ // Constructor.
+ public GatewayService(
+ IBeeGatewayClient beeGatewayClient,
+ IEthernaUserGatewayClient ethernaGatewayClient,
+ IIoService ioService)
+ {
+ this.beeGatewayClient = beeGatewayClient;
+ this.ethernaGatewayClient = ethernaGatewayClient;
+ this.ioService = ioService;
+ }
+
+ // Methods.
+ public async Task CalculateAmountAsync(TimeSpan ttl)
+ {
+ var currentPrice = await GetCurrentChainPriceAsync();
+ return (long)(ttl.TotalSeconds * currentPrice / CommonConsts.GnosisBlockTime.TotalSeconds);
+ }
+
+ public BzzBalance CalculateBzzPrice(long amount, int depth) =>
+ amount * Math.Pow(2, depth) / BzzDecimalPlacesToUnit;
+
+ public int CalculateDepth(long contentByteSize)
+ {
+ var batchDepth = 17;
+ while (Math.Pow(2, batchDepth) * ChunkByteSize < CalculateRequiredPostageBatchSpace(contentByteSize))
+ batchDepth++;
+ return batchDepth;
+ }
+
+ public long CalculatePostageBatchByteSize(PostageBatchDto postageBatch)
+ {
+ ArgumentNullException.ThrowIfNull(postageBatch, nameof(postageBatch));
+ return (long)Math.Pow(2, postageBatch.Depth) * ChunkByteSize;
+ }
+
+ public long CalculateRequiredPostageBatchSpace(long contentByteSize) =>
+ (long)(contentByteSize * 1.2); //keep 20% of tolerance
+
+ public async Task CreatePostageBatchAsync(long amount, int batchDepth, string? label)
+ {
+ if (amount <= 0)
+ throw new ArgumentException("Amount must be positive");
+ if (batchDepth < MinBatchDepth)
+ throw new ArgumentException($"Postage depth must be at least {MinBatchDepth}");
+
+ // Start creation.
+ var bzzPrice = CalculateBzzPrice(amount, batchDepth);
+ ioService.WriteLine($"Creating postage batch... Depth: {batchDepth}, Amount: {amount}, BZZ price: {bzzPrice}");
+ var batchReferenceId = await ethernaGatewayClient.UsersClient.BatchesPostAsync(batchDepth, amount, label);
+
+ // Wait until created batch is available.
+ ioService.Write("Waiting for batch created... (it may take a while)");
+
+ var batchStartWait = DateTime.UtcNow;
+ string? batchId = null;
+ do
+ {
+ //timeout throw exception
+ if (DateTime.UtcNow - batchStartWait >= BatchCreationTimeout)
+ {
+ var ex = new InvalidOperationException("Batch not avaiable after timeout");
+ ex.Data.Add("BatchReferenceId", batchReferenceId);
+ throw ex;
+ }
+
+ try
+ {
+ batchId = await ethernaGatewayClient.SystemClient.PostageBatchRefAsync(batchReferenceId);
+ }
+ catch (EthernaGatewayApiException)
+ {
+ //waiting for batchId available
+ await Task.Delay(BatchCheckTimeSpan);
+ }
+ } while (string.IsNullOrWhiteSpace(batchId));
+
+ ioService.WriteLine(". Done");
+
+ await WaitForBatchUsableAsync(batchId);
+
+ return batchId;
+ }
+
+ public Task FundResourcePinningAsync(string hash) =>
+ ethernaGatewayClient.ResourcesClient.PinPostAsync(hash);
+
+ public Task FundResourceTrafficAsync(string hash) =>
+ ethernaGatewayClient.ResourcesClient.OffersPostAsync(hash);
+
+ public async Task GetCurrentChainPriceAsync() =>
+ (await ethernaGatewayClient.SystemClient.ChainstateAsync()).CurrentPrice;
+
+ public Task GetPostageBatchInfoAsync(string batchId) =>
+ ethernaGatewayClient.UsersClient.BatchesGetAsync(batchId);
+
+ public Task UploadFileAsync(
+ string postageBatchId,
+ Stream content,
+ string? name,
+ string? contentType,
+ bool pinResource) =>
+ beeGatewayClient.UploadFileAsync(
+ postageBatchId,
+ content,
+ name: name,
+ contentType: contentType,
+ swarmDeferredUpload: true,
+ swarmPin: pinResource);
+
+ // Helpers.
+ private async Task WaitForBatchUsableAsync(string batchId)
+ {
+ // Wait until created batch is usable.
+ ioService.Write("Waiting for batch being usable... (it may take a while)");
+
+ var batchStartWait = DateTime.UtcNow;
+ bool batchIsUsable;
+ do
+ {
+ //timeout throw exception
+ if (DateTime.UtcNow - batchStartWait >= BatchUsableTimeout)
+ {
+ var ex = new InvalidOperationException("Batch not usable after timeout");
+ ex.Data.Add("BatchId", batchId);
+ throw ex;
+ }
+
+ batchIsUsable = (await GetPostageBatchInfoAsync(batchId)).Usable;
+
+ //waiting for batch usable
+ await Task.Delay(BatchCheckTimeSpan);
+ } while (!batchIsUsable);
+
+ ioService.WriteLine(". Done");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/IAuthenticationService.cs b/src/EthernaGatewayCli/Services/IAuthenticationService.cs
new file mode 100644
index 0000000..c7b7540
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/IAuthenticationService.cs
@@ -0,0 +1,23 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Services
+{
+ public interface IAuthenticationService
+ {
+ Task SignInAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/IFileService.cs b/src/EthernaGatewayCli/Services/IFileService.cs
new file mode 100644
index 0000000..28ab6b7
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/IFileService.cs
@@ -0,0 +1,25 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Etherna.GatewayCli.Services
+{
+ public interface IFileService
+ {
+ string GetMimeType(string filePath);
+
+ string GetMimeTypeFromContent(string filePath);
+
+ string GetMimeTypeFromExtension(string filePath);
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/IGatewayService.cs b/src/EthernaGatewayCli/Services/IGatewayService.cs
new file mode 100644
index 0000000..fca3630
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/IGatewayService.cs
@@ -0,0 +1,42 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.Domain;
+using Etherna.Sdk.GeneratedClients.Gateway;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Services
+{
+ public interface IGatewayService
+ {
+ Task CalculateAmountAsync(TimeSpan ttl);
+ BzzBalance CalculateBzzPrice(long amount, int depth);
+ int CalculateDepth(long contentByteSize);
+ long CalculatePostageBatchByteSize(PostageBatchDto postageBatch);
+ long CalculateRequiredPostageBatchSpace(long contentByteSize);
+ Task CreatePostageBatchAsync(long amount, int batchDepth, string? label);
+ Task FundResourcePinningAsync(string hash);
+ Task FundResourceTrafficAsync(string hash);
+ Task GetCurrentChainPriceAsync();
+ Task GetPostageBatchInfoAsync(string batchId);
+ Task UploadFileAsync(
+ string postageBatchId,
+ Stream content,
+ string? name,
+ string? contentType,
+ bool pinResource);
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Services/IIoService.cs b/src/EthernaGatewayCli/Services/IIoService.cs
new file mode 100644
index 0000000..7b66049
--- /dev/null
+++ b/src/EthernaGatewayCli/Services/IIoService.cs
@@ -0,0 +1,28 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+
+namespace Etherna.GatewayCli.Services
+{
+ public interface IIoService
+ {
+ ConsoleKeyInfo ReadKey();
+ string? ReadLine();
+ void Write(string? value);
+ void WriteError(string value);
+ void WriteErrorLine(string value);
+ void WriteLine(string? value = null);
+ }
+}
\ No newline at end of file
diff --git a/src/EthernaGatewayCli/Utilities/EthernaVersionControl.cs b/src/EthernaGatewayCli/Utilities/EthernaVersionControl.cs
new file mode 100644
index 0000000..d3bda7d
--- /dev/null
+++ b/src/EthernaGatewayCli/Utilities/EthernaVersionControl.cs
@@ -0,0 +1,106 @@
+// Copyright 2024-present Etherna SA
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using Etherna.GatewayCli.Models.GitHubDto;
+using Etherna.GatewayCli.Services;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Etherna.GatewayCli.Utilities
+{
+ public static class EthernaVersionControl
+ {
+ // Fields.
+ private static Version? _currentVersion;
+
+ // Properties.
+ [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations")]
+ public static Version CurrentVersion
+ {
+ get
+ {
+ if (_currentVersion is null)
+ {
+ var assemblyVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ??
+ throw new InvalidOperationException("Invalid assembly version");
+ _currentVersion = new Version(assemblyVersion);
+ }
+ return _currentVersion;
+ }
+ }
+
+ // Public methods.
+ public static async Task CheckNewVersionAsync(
+ IIoService ioService)
+ {
+ ArgumentNullException.ThrowIfNull(ioService, nameof(ioService));
+
+ // Get current version.
+ ioService.WriteLine($"Etherna Gateway CLI (v{CurrentVersion})");
+ ioService.WriteLine();
+
+ // Get last version form github releases.
+ try
+ {
+ using HttpClient httpClient = new();
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "EthernaImportClient");
+ var gitUrl = "https://api.github.com/repos/Etherna/etherna-gateway-cli/releases";
+ var response = await httpClient.GetAsync(gitUrl);
+ var gitReleaseVersionsDto = await response.Content.ReadFromJsonAsync>();
+
+ if (gitReleaseVersionsDto is null || gitReleaseVersionsDto.Count == 0)
+ return false;
+
+ var lastVersion = gitReleaseVersionsDto
+ .Select(git => new
+ {
+ Version = new Version(git.Tag_name.Replace("v", "", StringComparison.OrdinalIgnoreCase)),
+ Url = git.Html_url
+ })
+ .OrderByDescending(v => v.Version)
+ .First();
+
+ if (lastVersion.Version > CurrentVersion)
+ {
+ ioService.WriteLine(
+ $"""
+ A new release is available: {lastVersion.Version}
+ Upgrade now, or check out the release page at:
+ {lastVersion.Url}
+ """);
+ return true;
+ }
+ else
+ return false;
+ }
+#pragma warning disable CA1031
+ catch (Exception ex)
+#pragma warning restore CA1031
+ {
+ ioService.WriteErrorLine(
+ $"""
+ Unable to check last version on GitHub
+ Error: {ex.Message}
+ """);
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file