mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: proposal: custom node commands (#1007)
* feat: custom node command execution models and methods
* feat: implement custom node command handlers for HTTP server and Wails
* chore: expose GetNodeCommands API methods in HTTP and Wails
* chore: add sample custom node command implementation for Cashu restore
* feat: add frontend for custom node commands
* chore: consistent naming of custom node command entities
* chore: consistent naming of custom node command entities
* chore: return interface{} as custom node command execution result
* test: add tests for ParseCommandLine
* chore: add extra ParseCommandLine tests for json
* test: add API tests and mocks
* fix: stabilize order of arguments when invoking custom node commands
* test: add test for unsupported custom node command argument
* chore: add the mockery configuration file and move mocks to tests/mocks
* docs: document mockery usage
---------
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
parent
82e4a162fb
commit
d799bdff76
21 changed files with 3195 additions and 7 deletions
16
.mockery.yaml
Normal file
16
.mockery.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
filename: "{{.InterfaceName}}.go"
|
||||
dir: tests/mocks
|
||||
outpkg: mocks
|
||||
|
||||
# Fix deprecation warnings:
|
||||
issue-845-fix: True
|
||||
resolve-type-alias: False
|
||||
|
||||
packages:
|
||||
github.com/getAlby/hub/service:
|
||||
interfaces:
|
||||
Service:
|
||||
|
||||
github.com/getAlby/hub/lnclient:
|
||||
interfaces:
|
||||
LNClient:
|
||||
|
|
@ -95,6 +95,14 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco
|
|||
|
||||
$ go test ./... -run TestHandleGetInfoEvent
|
||||
|
||||
#### Mocking
|
||||
|
||||
We use [testify/mock](https://github.com/stretchr/testify) to facilitate mocking in tests. Instead of writing mocks manually, we generate them using [vektra/mockery](https://github.com/vektra/mockery). To regenerate them, [install mockery](https://vektra.github.io/mockery/latest/installation) and run it in the project's root directory:
|
||||
|
||||
$ mockery
|
||||
|
||||
Mockery loads its configuration from the .mockery.yaml file in the root directory of this project. To add mocks for new interfaces, add them to the configuration file and run mockery.
|
||||
|
||||
### Profiling
|
||||
|
||||
The application supports both the Go pprof library and the DataDog profiler.
|
||||
|
|
|
|||
88
api/api.go
88
api/api.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -1068,6 +1069,93 @@ func (api *api) Health(ctx context.Context) (*HealthResponse, error) {
|
|||
return &HealthResponse{Alarms: alarms}, nil
|
||||
}
|
||||
|
||||
func (api *api) GetCustomNodeCommands() (*CustomNodeCommandsResponse, error) {
|
||||
lnClient := api.svc.GetLNClient()
|
||||
if lnClient == nil {
|
||||
return nil, errors.New("LNClient not started")
|
||||
}
|
||||
|
||||
allCommandDefs := lnClient.GetCustomNodeCommandDefinitions()
|
||||
commandDefs := make([]CustomNodeCommandDef, 0, len(allCommandDefs))
|
||||
for _, commandDef := range allCommandDefs {
|
||||
argDefs := make([]CustomNodeCommandArgDef, 0, len(commandDef.Args))
|
||||
for _, argDef := range commandDef.Args {
|
||||
argDefs = append(argDefs, CustomNodeCommandArgDef{
|
||||
Name: argDef.Name,
|
||||
Description: argDef.Description,
|
||||
})
|
||||
}
|
||||
commandDefs = append(commandDefs, CustomNodeCommandDef{
|
||||
Name: commandDef.Name,
|
||||
Description: commandDef.Description,
|
||||
Args: argDefs,
|
||||
})
|
||||
}
|
||||
|
||||
return &CustomNodeCommandsResponse{Commands: commandDefs}, nil
|
||||
}
|
||||
|
||||
func (api *api) ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error) {
|
||||
lnClient := api.svc.GetLNClient()
|
||||
if lnClient == nil {
|
||||
return nil, errors.New("LNClient not started")
|
||||
}
|
||||
|
||||
// Split command line into arguments. Command name must be the first argument.
|
||||
parsedArgs, err := utils.ParseCommandLine(command)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse node command: %w", err)
|
||||
} else if len(parsedArgs) == 0 {
|
||||
return nil, errors.New("no command provided")
|
||||
}
|
||||
|
||||
// Look up the requested command definition.
|
||||
allCommandDefs := lnClient.GetCustomNodeCommandDefinitions()
|
||||
commandDefIdx := slices.IndexFunc(allCommandDefs, func(def lnclient.CustomNodeCommandDef) bool {
|
||||
return def.Name == parsedArgs[0]
|
||||
})
|
||||
if commandDefIdx < 0 {
|
||||
return nil, fmt.Errorf("unknown command: %q", parsedArgs[0])
|
||||
}
|
||||
|
||||
// Build flag set.
|
||||
commandDef := allCommandDefs[commandDefIdx]
|
||||
flagSet := flag.NewFlagSet(commandDef.Name, flag.ContinueOnError)
|
||||
for _, argDef := range commandDef.Args {
|
||||
flagSet.String(argDef.Name, "", argDef.Description)
|
||||
}
|
||||
|
||||
if err = flagSet.Parse(parsedArgs[1:]); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse command arguments: %w", err)
|
||||
}
|
||||
|
||||
// Collect flags that have been set.
|
||||
argValues := make(map[string]string)
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
argValues[f.Name] = f.Value.String()
|
||||
})
|
||||
|
||||
reqArgs := make([]lnclient.CustomNodeCommandArg, 0, len(argValues))
|
||||
for _, argDef := range commandDef.Args {
|
||||
if argValue, ok := argValues[argDef.Name]; ok {
|
||||
reqArgs = append(reqArgs, lnclient.CustomNodeCommandArg{
|
||||
Name: argDef.Name,
|
||||
Value: argValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nodeResp, err := lnClient.ExecuteCustomNodeCommand(ctx, &lnclient.CustomNodeCommandRequest{
|
||||
Name: commandDef.Name,
|
||||
Args: reqArgs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node failed to execute custom command: %w", err)
|
||||
}
|
||||
|
||||
return nodeResp.Response, nil
|
||||
}
|
||||
|
||||
func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) {
|
||||
var expiresAt *time.Time
|
||||
if expiresAtString != "" {
|
||||
|
|
|
|||
228
api/api_test.go
Normal file
228
api/api_test.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/service"
|
||||
"github.com/getAlby/hub/tests/mocks"
|
||||
)
|
||||
|
||||
func TestGetCustomNodeCommandDefinitions(t *testing.T) {
|
||||
lnClient := mocks.NewMockLNClient(t)
|
||||
svc := mocks.NewMockService(t)
|
||||
|
||||
mockLNCommandDefs := []lnclient.CustomNodeCommandDef{
|
||||
{
|
||||
Name: "no_args",
|
||||
Description: "command without args",
|
||||
Args: nil,
|
||||
},
|
||||
{
|
||||
Name: "with_args",
|
||||
Description: "command with args",
|
||||
Args: []lnclient.CustomNodeCommandArgDef{
|
||||
{Name: "arg1", Description: "first argument"},
|
||||
{Name: "arg2", Description: "second argument"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedCommands := []CustomNodeCommandDef{
|
||||
{
|
||||
Name: "no_args",
|
||||
Description: "command without args",
|
||||
Args: []CustomNodeCommandArgDef{},
|
||||
},
|
||||
{
|
||||
Name: "with_args",
|
||||
Description: "command with args",
|
||||
Args: []CustomNodeCommandArgDef{
|
||||
{Name: "arg1", Description: "first argument"},
|
||||
{Name: "arg2", Description: "second argument"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lnClient.On("GetCustomNodeCommandDefinitions").Return(mockLNCommandDefs)
|
||||
svc.On("GetLNClient").Return(lnClient)
|
||||
|
||||
theAPI := instantiateAPIWithService(svc)
|
||||
|
||||
commands, err := theAPI.GetCustomNodeCommands()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, commands)
|
||||
require.ElementsMatch(t, expectedCommands, commands.Commands)
|
||||
}
|
||||
|
||||
func TestExecuteCustomNodeCommand(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
apiCommandLine string
|
||||
lnSupportedCommands []lnclient.CustomNodeCommandDef
|
||||
lnExpectedCommandReq *lnclient.CustomNodeCommandRequest
|
||||
lnResponse *lnclient.CustomNodeCommandResponse
|
||||
lnError error
|
||||
apiExpectedResponse interface{}
|
||||
apiExpectedErr string
|
||||
}
|
||||
|
||||
// Successful execution of a command without args.
|
||||
testCaseOkNoArgs := testCase{
|
||||
name: "command without args",
|
||||
apiCommandLine: "test_command",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
|
||||
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
|
||||
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
|
||||
lnError: nil,
|
||||
apiExpectedResponse: "ok",
|
||||
apiExpectedErr: "",
|
||||
}
|
||||
|
||||
// Successful execution of a command with args. The command line contains
|
||||
// different arg value styles: with '=' and with space.
|
||||
testCaseOkWithArgs := testCase{
|
||||
name: "command with args",
|
||||
apiCommandLine: "test_command --arg1=foo --arg2 bar",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{
|
||||
{
|
||||
Name: "test_command",
|
||||
Args: []lnclient.CustomNodeCommandArgDef{
|
||||
{Name: "arg1", Description: "argument one"},
|
||||
{Name: "arg2", Description: "argument two"},
|
||||
},
|
||||
},
|
||||
},
|
||||
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{
|
||||
{Name: "arg1", Value: "foo"},
|
||||
{Name: "arg2", Value: "bar"},
|
||||
}},
|
||||
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
|
||||
lnError: nil,
|
||||
apiExpectedResponse: "ok",
|
||||
apiExpectedErr: "",
|
||||
}
|
||||
|
||||
// Successful execution of a command with a possible but unset arg.
|
||||
testCaseOkWithUnsetArg := testCase{
|
||||
name: "command with unset arg",
|
||||
apiCommandLine: "test_command",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{
|
||||
{Name: "test_command", Args: []lnclient.CustomNodeCommandArgDef{{Name: "arg1", Description: "argument one"}}},
|
||||
},
|
||||
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
|
||||
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
|
||||
lnError: nil,
|
||||
apiExpectedResponse: "ok",
|
||||
apiExpectedErr: "",
|
||||
}
|
||||
|
||||
// Error: command line is empty.
|
||||
testCaseErrEmptyCommand := testCase{
|
||||
name: "empty command",
|
||||
apiCommandLine: "",
|
||||
lnSupportedCommands: nil,
|
||||
lnExpectedCommandReq: nil,
|
||||
lnResponse: nil,
|
||||
lnError: nil,
|
||||
apiExpectedResponse: nil,
|
||||
apiExpectedErr: "no command provided",
|
||||
}
|
||||
|
||||
// Error: command line is malformed, i.e. non-parseable.
|
||||
testCaseErrMalformedCommand := testCase{
|
||||
name: "command with unclosed quote",
|
||||
apiCommandLine: "test_command\"",
|
||||
lnSupportedCommands: nil,
|
||||
lnExpectedCommandReq: nil,
|
||||
lnResponse: nil,
|
||||
lnError: nil,
|
||||
apiExpectedResponse: nil,
|
||||
apiExpectedErr: "failed to parse node command",
|
||||
}
|
||||
|
||||
// Error: node does not support this command.
|
||||
testCaseErrUnknownCommand := testCase{
|
||||
name: "unknown command",
|
||||
apiCommandLine: "test_command_unknown",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
|
||||
lnExpectedCommandReq: nil,
|
||||
lnResponse: nil,
|
||||
lnError: nil,
|
||||
apiExpectedResponse: nil,
|
||||
apiExpectedErr: "unknown command",
|
||||
}
|
||||
|
||||
// Error: unsupported command argument.
|
||||
testCaseErrUnknownArg := testCase{
|
||||
name: "unknown argument",
|
||||
apiCommandLine: "test_command --unknown=fail",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
|
||||
lnExpectedCommandReq: nil,
|
||||
lnResponse: nil,
|
||||
lnError: nil,
|
||||
apiExpectedResponse: nil,
|
||||
apiExpectedErr: "flag provided but not defined: -unknown",
|
||||
}
|
||||
|
||||
// Error: the command is valid but the node fails to execute it.
|
||||
testCaseErrNodeFailed := testCase{
|
||||
name: "node failed to execute command",
|
||||
apiCommandLine: "test_command",
|
||||
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
|
||||
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
|
||||
lnResponse: nil,
|
||||
lnError: fmt.Errorf("utter failure"),
|
||||
apiExpectedResponse: nil,
|
||||
apiExpectedErr: "utter failure",
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
testCaseOkNoArgs,
|
||||
testCaseOkWithArgs,
|
||||
testCaseOkWithUnsetArg,
|
||||
testCaseErrEmptyCommand,
|
||||
testCaseErrMalformedCommand,
|
||||
testCaseErrUnknownCommand,
|
||||
testCaseErrUnknownArg,
|
||||
testCaseErrNodeFailed,
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
lnClient := mocks.NewMockLNClient(t)
|
||||
svc := mocks.NewMockService(t)
|
||||
|
||||
if tc.lnSupportedCommands != nil {
|
||||
lnClient.On("GetCustomNodeCommandDefinitions").Return(tc.lnSupportedCommands)
|
||||
}
|
||||
|
||||
if tc.lnExpectedCommandReq != nil {
|
||||
lnClient.On("ExecuteCustomNodeCommand", mock.Anything, tc.lnExpectedCommandReq).Return(tc.lnResponse, tc.lnError)
|
||||
}
|
||||
|
||||
svc.On("GetLNClient").Return(lnClient)
|
||||
|
||||
theAPI := instantiateAPIWithService(svc)
|
||||
|
||||
response, err := theAPI.ExecuteCustomNodeCommand(context.TODO(), tc.apiCommandLine)
|
||||
require.Equal(t, tc.apiExpectedResponse, response)
|
||||
if tc.apiExpectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.apiExpectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// instantiateAPIWithService is a helper function that returns a partially
|
||||
// constructed API instance. It is only suitable for the simplest of test cases.
|
||||
func instantiateAPIWithService(s service.Service) *api {
|
||||
return &api{svc: s}
|
||||
}
|
||||
|
|
@ -57,6 +57,8 @@ type API interface {
|
|||
MigrateNodeStorage(ctx context.Context, to string) error
|
||||
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
|
||||
Health(ctx context.Context) (*HealthResponse, error)
|
||||
GetCustomNodeCommands() (*CustomNodeCommandsResponse, error)
|
||||
ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
|
|
@ -392,3 +394,22 @@ func NewHealthAlarm(kind HealthAlarmKind, rawDetails any) HealthAlarm {
|
|||
type HealthResponse struct {
|
||||
Alarms []HealthAlarm `json:"alarms,omitempty"`
|
||||
}
|
||||
|
||||
type CustomNodeCommandArgDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type CustomNodeCommandDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Args []CustomNodeCommandArgDef `json:"args"`
|
||||
}
|
||||
|
||||
type CustomNodeCommandsResponse struct {
|
||||
Commands []CustomNodeCommandDef `json:"commands"`
|
||||
}
|
||||
|
||||
type ExecuteCustomNodeCommandRequest struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import React from "react";
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "src/components/ui/alert-dialog";
|
||||
import { Textarea } from "src/components/ui/textarea";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
type ExecuteCustomNodeCommandDialogContentProps = {
|
||||
availableCommands: string;
|
||||
setCommandResponse: (response: string) => void;
|
||||
};
|
||||
|
||||
export function ExecuteCustomNodeCommandDialogContent({
|
||||
setCommandResponse,
|
||||
availableCommands,
|
||||
}: ExecuteCustomNodeCommandDialogContentProps) {
|
||||
const { mutate: reloadInfo } = useInfo();
|
||||
const { toast } = useToast();
|
||||
const [command, setCommand] = React.useState<string>();
|
||||
|
||||
let parsedAvailableCommands = availableCommands;
|
||||
try {
|
||||
parsedAvailableCommands = JSON.stringify(
|
||||
JSON.parse(availableCommands).commands,
|
||||
null,
|
||||
2
|
||||
);
|
||||
} catch (error) {
|
||||
// ignore unexpected json
|
||||
}
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (!command) {
|
||||
throw new Error("No command set");
|
||||
}
|
||||
const result = await request("/api/command", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
await reloadInfo();
|
||||
|
||||
const parsedResponse = JSON.stringify(result);
|
||||
setCommandResponse(parsedResponse);
|
||||
|
||||
toast({ title: "Command executed", description: parsedResponse });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Something went wrong: " + error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialogContent>
|
||||
<form onSubmit={onSubmit}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Execute Custom Node Command</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-left">
|
||||
<Textarea
|
||||
className="h-36 font-mono"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder="commandname --arg1=value1"
|
||||
/>
|
||||
<p className="mt-2">Available commands</p>
|
||||
<Textarea
|
||||
readOnly
|
||||
className="mt-2 font-mono"
|
||||
value={parsedAvailableCommands}
|
||||
rows={10}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="mt-4">
|
||||
<AlertDialogCancel onClick={() => setCommand("")}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction type="submit">Execute</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</AlertDialogContent>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { ExecuteCustomNodeCommandDialogContent } from "src/components/ExecuteCustomNodeCommandDialogContent";
|
||||
import { ResetRoutingDataDialogContent } from "src/components/ResetRoutingDataDialogContent";
|
||||
import SettingsHeader from "src/components/SettingsHeader";
|
||||
import {
|
||||
|
|
@ -221,6 +222,7 @@ export default function DebugTools() {
|
|||
| "getNodeLogs"
|
||||
| "getNetworkGraph"
|
||||
| "resetRoutingData"
|
||||
| "customNodeCommand"
|
||||
>();
|
||||
|
||||
const { data: info } = useInfo();
|
||||
|
|
@ -311,6 +313,23 @@ export default function DebugTools() {
|
|||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
apiRequest(`/api/commands`, "GET");
|
||||
}}
|
||||
>
|
||||
Get Node Commands
|
||||
</Button>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
apiRequest(`/api/commands`, "GET");
|
||||
setDialog("customNodeCommand");
|
||||
}}
|
||||
>
|
||||
Execute Node Command
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
{/* probing functions are not useful */}
|
||||
{/*info?.backendType === "LDK" && (
|
||||
<AlertDialogTrigger asChild>
|
||||
|
|
@ -343,6 +362,12 @@ export default function DebugTools() {
|
|||
<GetNetworkGraphDialogContent apiRequest={apiRequest} />
|
||||
)}
|
||||
{dialog === "resetRoutingData" && <ResetRoutingDataDialogContent />}
|
||||
{dialog === "customNodeCommand" && (
|
||||
<ExecuteCustomNodeCommandDialogContent
|
||||
availableCommands={apiResponse}
|
||||
setCommandResponse={setApiResponse}
|
||||
/>
|
||||
)}
|
||||
</AlertDialog>
|
||||
</div>
|
||||
{apiResponse && (
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
restrictedGroup.POST("/api/send-spontaneous-payment-probes", httpSvc.sendSpontaneousPaymentProbesHandler)
|
||||
restrictedGroup.GET("/api/log/:type", httpSvc.getLogOutputHandler)
|
||||
restrictedGroup.GET("/api/health", httpSvc.healthHandler)
|
||||
restrictedGroup.GET("/api/commands", httpSvc.getCustomNodeCommandsHandler)
|
||||
restrictedGroup.POST("/api/command", httpSvc.execCustomNodeCommandHandler)
|
||||
|
||||
httpSvc.albyHttpSvc.RegisterSharedRoutes(restrictedGroup, e)
|
||||
}
|
||||
|
|
@ -999,6 +1001,35 @@ func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, getLogResponse)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) getCustomNodeCommandsHandler(c echo.Context) error {
|
||||
nodeCommandsResponse, err := httpSvc.api.GetCustomNodeCommands()
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to get node commands: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, nodeCommandsResponse)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) execCustomNodeCommandHandler(c echo.Context) error {
|
||||
var execCommandRequest api.ExecuteCustomNodeCommandRequest
|
||||
if err := c.Bind(&execCommandRequest); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
execCommandResponse, err := httpSvc.api.ExecuteCustomNodeCommand(c.Request().Context(), execCommandRequest.Command)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to execute command: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, execCommandResponse)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
|
||||
redirectUrl := httpSvc.cfg.GetEnv().FrontendUrl
|
||||
if redirectUrl == "" {
|
||||
|
|
|
|||
|
|
@ -493,3 +493,11 @@ func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string {
|
|||
func (bs *BreezService) GetPubkey() string {
|
||||
return bs.pubkey
|
||||
}
|
||||
|
||||
func (bs *BreezService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BreezService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,16 @@ import (
|
|||
|
||||
"github.com/elnosh/gonuts/wallet"
|
||||
"github.com/elnosh/gonuts/wallet/storage"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
)
|
||||
|
||||
const nodeCommandRestore = "restore"
|
||||
const exampleCommandWithArg = "example"
|
||||
|
||||
type CashuService struct {
|
||||
wallet *wallet.Wallet
|
||||
}
|
||||
|
|
@ -375,3 +379,67 @@ func (cs *CashuService) GetSupportedNIP47NotificationTypes() []string {
|
|||
func (svc *CashuService) GetPubkey() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cs *CashuService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return []lnclient.CustomNodeCommandDef{
|
||||
{
|
||||
Name: nodeCommandRestore,
|
||||
Description: "Restore cashu tokens after the wallet had a stuck payment.",
|
||||
Args: nil,
|
||||
},
|
||||
{
|
||||
Name: exampleCommandWithArg,
|
||||
Description: "Example command with argument",
|
||||
Args: []lnclient.CustomNodeCommandArgDef{
|
||||
{
|
||||
Name: "hello",
|
||||
Description: "world",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *CashuService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
switch command.Name {
|
||||
case nodeCommandRestore:
|
||||
return cs.executeCommandRestore(ctx)
|
||||
case exampleCommandWithArg:
|
||||
if len(command.Args) != 1 {
|
||||
return nil, errors.New("please provide an argument")
|
||||
}
|
||||
|
||||
return &lnclient.CustomNodeCommandResponse{
|
||||
Response: map[string]string{
|
||||
command.Args[0].Name: command.Args[0].Value,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, lnclient.ErrUnknownCustomNodeCommand
|
||||
}
|
||||
|
||||
func (cs *CashuService) executeCommandRestore(ctx context.Context) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
// FIXME: needs latest Cashu changes to be merged
|
||||
// mnemonic := cs.wallet.Mnemonic()
|
||||
// currentMint := cs.wallet.CurrentMint()
|
||||
//
|
||||
// if err := cs.wallet.Shutdown(); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// if err := os.RemoveAll(cs.workDir); err != nil {
|
||||
// logger.Logger.WithError(err).Error("Failed to remove wallet directory")
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// amountRestored, err := wallet.Restore(cs.workDir, mnemonic, []string{currentMint})
|
||||
// if err != nil {
|
||||
// logger.Logger.WithError(err).Error("Failed restore cashu wallet")
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// logger.Logger.WithField("amountRestored", amountRestored).Info("Successfully restored cashu wallet")
|
||||
|
||||
return lnclient.NewCustomNodeCommandResponseEmpty(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -689,3 +689,11 @@ func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string {
|
|||
func (gs *GreenlightService) GetPubkey() string {
|
||||
return gs.pubkey
|
||||
}
|
||||
|
||||
func (gs *GreenlightService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *GreenlightService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1736,6 +1736,14 @@ func (ls *LDKService) GetPubkey() string {
|
|||
return ls.pubkey
|
||||
}
|
||||
|
||||
func (ls *LDKService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getEncodedChannelMonitorsFromStaticChannelsBackup(channelsBackup *events.StaticChannelsBackupEvent) []ldk_node.KeyValue {
|
||||
encodedMonitors := []ldk_node.KeyValue{}
|
||||
for _, monitor := range channelsBackup.Monitors {
|
||||
|
|
|
|||
|
|
@ -1253,3 +1253,11 @@ func (svc *LNDService) GetStorageDir() (string, error) {
|
|||
}
|
||||
|
||||
func (svc *LNDService) UpdateLastWalletSyncRequest() {}
|
||||
|
||||
func (svc *LNDService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *LNDService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package lnclient
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TODO: remove JSON tags from these models (LNClient models should not be exposed directly)
|
||||
|
|
@ -77,6 +78,8 @@ type LNClient interface {
|
|||
UpdateLastWalletSyncRequest()
|
||||
GetSupportedNIP47Methods() []string
|
||||
GetSupportedNIP47NotificationTypes() []string
|
||||
GetCustomNodeCommandDefinitions() []CustomNodeCommandDef
|
||||
ExecuteCustomNodeCommand(ctx context.Context, command *CustomNodeCommandRequest) (*CustomNodeCommandResponse, error)
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
|
|
@ -189,6 +192,39 @@ type PaymentFailedEventProperties struct {
|
|||
Reason string
|
||||
}
|
||||
|
||||
type CustomNodeCommandArgDef struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type CustomNodeCommandDef struct {
|
||||
Name string
|
||||
Description string
|
||||
Args []CustomNodeCommandArgDef
|
||||
}
|
||||
|
||||
type CustomNodeCommandArg struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type CustomNodeCommandRequest struct {
|
||||
Name string
|
||||
Args []CustomNodeCommandArg
|
||||
}
|
||||
|
||||
type CustomNodeCommandResponse struct {
|
||||
Response interface{}
|
||||
}
|
||||
|
||||
func NewCustomNodeCommandResponseEmpty() *CustomNodeCommandResponse {
|
||||
return &CustomNodeCommandResponse{
|
||||
Response: struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
var ErrUnknownCustomNodeCommand = errors.New("unknown custom node command")
|
||||
|
||||
// default invoice expiry in seconds (1 day)
|
||||
const DEFAULT_INVOICE_EXPIRY = 86400
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -541,3 +542,11 @@ func (svc *PhoenixService) GetSupportedNIP47NotificationTypes() []string {
|
|||
func (svc *PhoenixService) GetPubkey() string {
|
||||
return svc.pubkey
|
||||
}
|
||||
|
||||
func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,3 +203,11 @@ func (mln *MockLn) GetPubkey() string {
|
|||
|
||||
return "123pubkey"
|
||||
}
|
||||
|
||||
func (mln *MockLn) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mln *MockLn) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
1817
tests/mocks/LNClient.go
Normal file
1817
tests/mocks/LNClient.go
Normal file
File diff suppressed because it is too large
Load diff
531
tests/mocks/Service.go
Normal file
531
tests/mocks/Service.go
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
alby "github.com/getAlby/hub/alby"
|
||||
config "github.com/getAlby/hub/config"
|
||||
|
||||
events "github.com/getAlby/hub/events"
|
||||
|
||||
gorm "gorm.io/gorm"
|
||||
|
||||
keys "github.com/getAlby/hub/service/keys"
|
||||
|
||||
lnclient "github.com/getAlby/hub/lnclient"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
transactions "github.com/getAlby/hub/transactions"
|
||||
)
|
||||
|
||||
// MockService is an autogenerated mock type for the Service type
|
||||
type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockService) EXPECT() *MockService_Expecter {
|
||||
return &MockService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetAlbyOAuthSvc provides a mock function with no fields
|
||||
func (_m *MockService) GetAlbyOAuthSvc() alby.AlbyOAuthService {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAlbyOAuthSvc")
|
||||
}
|
||||
|
||||
var r0 alby.AlbyOAuthService
|
||||
if rf, ok := ret.Get(0).(func() alby.AlbyOAuthService); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(alby.AlbyOAuthService)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetAlbyOAuthSvc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAlbyOAuthSvc'
|
||||
type MockService_GetAlbyOAuthSvc_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetAlbyOAuthSvc is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetAlbyOAuthSvc() *MockService_GetAlbyOAuthSvc_Call {
|
||||
return &MockService_GetAlbyOAuthSvc_Call{Call: _e.mock.On("GetAlbyOAuthSvc")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbyOAuthSvc_Call) Run(run func()) *MockService_GetAlbyOAuthSvc_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbyOAuthSvc_Call) Return(_a0 alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbyOAuthSvc_Call) RunAndReturn(run func() alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetConfig provides a mock function with no fields
|
||||
func (_m *MockService) GetConfig() config.Config {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetConfig")
|
||||
}
|
||||
|
||||
var r0 config.Config
|
||||
if rf, ok := ret.Get(0).(func() config.Config); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(config.Config)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConfig'
|
||||
type MockService_GetConfig_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetConfig is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetConfig() *MockService_GetConfig_Call {
|
||||
return &MockService_GetConfig_Call{Call: _e.mock.On("GetConfig")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetConfig_Call) Run(run func()) *MockService_GetConfig_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetConfig_Call) Return(_a0 config.Config) *MockService_GetConfig_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetConfig_Call) RunAndReturn(run func() config.Config) *MockService_GetConfig_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetDB provides a mock function with no fields
|
||||
func (_m *MockService) GetDB() *gorm.DB {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetDB")
|
||||
}
|
||||
|
||||
var r0 *gorm.DB
|
||||
if rf, ok := ret.Get(0).(func() *gorm.DB); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*gorm.DB)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDB'
|
||||
type MockService_GetDB_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetDB is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetDB() *MockService_GetDB_Call {
|
||||
return &MockService_GetDB_Call{Call: _e.mock.On("GetDB")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetDB_Call) Run(run func()) *MockService_GetDB_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetDB_Call) Return(_a0 *gorm.DB) *MockService_GetDB_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetDB_Call) RunAndReturn(run func() *gorm.DB) *MockService_GetDB_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetEventPublisher provides a mock function with no fields
|
||||
func (_m *MockService) GetEventPublisher() events.EventPublisher {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetEventPublisher")
|
||||
}
|
||||
|
||||
var r0 events.EventPublisher
|
||||
if rf, ok := ret.Get(0).(func() events.EventPublisher); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(events.EventPublisher)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetEventPublisher_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEventPublisher'
|
||||
type MockService_GetEventPublisher_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetEventPublisher is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetEventPublisher() *MockService_GetEventPublisher_Call {
|
||||
return &MockService_GetEventPublisher_Call{Call: _e.mock.On("GetEventPublisher")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetEventPublisher_Call) Run(run func()) *MockService_GetEventPublisher_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetEventPublisher_Call) Return(_a0 events.EventPublisher) *MockService_GetEventPublisher_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetEventPublisher_Call) RunAndReturn(run func() events.EventPublisher) *MockService_GetEventPublisher_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetKeys provides a mock function with no fields
|
||||
func (_m *MockService) GetKeys() keys.Keys {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetKeys")
|
||||
}
|
||||
|
||||
var r0 keys.Keys
|
||||
if rf, ok := ret.Get(0).(func() keys.Keys); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(keys.Keys)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKeys'
|
||||
type MockService_GetKeys_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetKeys is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetKeys() *MockService_GetKeys_Call {
|
||||
return &MockService_GetKeys_Call{Call: _e.mock.On("GetKeys")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetKeys_Call) Run(run func()) *MockService_GetKeys_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetKeys_Call) Return(_a0 keys.Keys) *MockService_GetKeys_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetKeys_Call) RunAndReturn(run func() keys.Keys) *MockService_GetKeys_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetLNClient provides a mock function with no fields
|
||||
func (_m *MockService) GetLNClient() lnclient.LNClient {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLNClient")
|
||||
}
|
||||
|
||||
var r0 lnclient.LNClient
|
||||
if rf, ok := ret.Get(0).(func() lnclient.LNClient); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(lnclient.LNClient)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetLNClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLNClient'
|
||||
type MockService_GetLNClient_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetLNClient is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetLNClient() *MockService_GetLNClient_Call {
|
||||
return &MockService_GetLNClient_Call{Call: _e.mock.On("GetLNClient")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetLNClient_Call) Run(run func()) *MockService_GetLNClient_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetLNClient_Call) Return(_a0 lnclient.LNClient) *MockService_GetLNClient_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetLNClient_Call) RunAndReturn(run func() lnclient.LNClient) *MockService_GetLNClient_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetTransactionsService provides a mock function with no fields
|
||||
func (_m *MockService) GetTransactionsService() transactions.TransactionsService {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetTransactionsService")
|
||||
}
|
||||
|
||||
var r0 transactions.TransactionsService
|
||||
if rf, ok := ret.Get(0).(func() transactions.TransactionsService); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(transactions.TransactionsService)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetTransactionsService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransactionsService'
|
||||
type MockService_GetTransactionsService_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetTransactionsService is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetTransactionsService() *MockService_GetTransactionsService_Call {
|
||||
return &MockService_GetTransactionsService_Call{Call: _e.mock.On("GetTransactionsService")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetTransactionsService_Call) Run(run func()) *MockService_GetTransactionsService_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetTransactionsService_Call) Return(_a0 transactions.TransactionsService) *MockService_GetTransactionsService_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetTransactionsService_Call) RunAndReturn(run func() transactions.TransactionsService) *MockService_GetTransactionsService_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// IsRelayReady provides a mock function with no fields
|
||||
func (_m *MockService) IsRelayReady() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IsRelayReady")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_IsRelayReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsRelayReady'
|
||||
type MockService_IsRelayReady_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IsRelayReady is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) IsRelayReady() *MockService_IsRelayReady_Call {
|
||||
return &MockService_IsRelayReady_Call{Call: _e.mock.On("IsRelayReady")}
|
||||
}
|
||||
|
||||
func (_c *MockService_IsRelayReady_Call) Run(run func()) *MockService_IsRelayReady_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_IsRelayReady_Call) Return(_a0 bool) *MockService_IsRelayReady_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_IsRelayReady_Call) RunAndReturn(run func() bool) *MockService_IsRelayReady_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Shutdown provides a mock function with no fields
|
||||
func (_m *MockService) Shutdown() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockService_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown'
|
||||
type MockService_Shutdown_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Shutdown is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) Shutdown() *MockService_Shutdown_Call {
|
||||
return &MockService_Shutdown_Call{Call: _e.mock.On("Shutdown")}
|
||||
}
|
||||
|
||||
func (_c *MockService_Shutdown_Call) Run(run func()) *MockService_Shutdown_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Shutdown_Call) Return() *MockService_Shutdown_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Shutdown_Call) RunAndReturn(run func()) *MockService_Shutdown_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// StartApp provides a mock function with given fields: encryptionKey
|
||||
func (_m *MockService) StartApp(encryptionKey string) error {
|
||||
ret := _m.Called(encryptionKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for StartApp")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(encryptionKey)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_StartApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartApp'
|
||||
type MockService_StartApp_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// StartApp is a helper method to define mock.On call
|
||||
// - encryptionKey string
|
||||
func (_e *MockService_Expecter) StartApp(encryptionKey interface{}) *MockService_StartApp_Call {
|
||||
return &MockService_StartApp_Call{Call: _e.mock.On("StartApp", encryptionKey)}
|
||||
}
|
||||
|
||||
func (_c *MockService_StartApp_Call) Run(run func(encryptionKey string)) *MockService_StartApp_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_StartApp_Call) Return(_a0 error) *MockService_StartApp_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_StartApp_Call) RunAndReturn(run func(string) error) *MockService_StartApp_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// StopApp provides a mock function with no fields
|
||||
func (_m *MockService) StopApp() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockService_StopApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StopApp'
|
||||
type MockService_StopApp_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// StopApp is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) StopApp() *MockService_StopApp_Call {
|
||||
return &MockService_StopApp_Call{Call: _e.mock.On("StopApp")}
|
||||
}
|
||||
|
||||
func (_c *MockService_StopApp_Call) Run(run func()) *MockService_StopApp_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_StopApp_Call) Return() *MockService_StopApp_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_StopApp_Call) RunAndReturn(run func()) *MockService_StopApp_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockService {
|
||||
mock := &MockService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func ReadFileTail(filePath string, maxLen int) (data []byte, err error) {
|
||||
|
|
@ -55,3 +57,39 @@ func Filter[T any](s []T, f func(T) bool) []T {
|
|||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func ParseCommandLine(s string) ([]string, error) {
|
||||
args := make([]string, 0)
|
||||
var currentArg strings.Builder
|
||||
inQuotes := false
|
||||
escaped := false
|
||||
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case escaped:
|
||||
currentArg.WriteRune(r)
|
||||
escaped = false
|
||||
case r == '\\':
|
||||
escaped = true
|
||||
case r == '"':
|
||||
inQuotes = !inQuotes
|
||||
case unicode.IsSpace(r) && !inQuotes:
|
||||
if currentArg.Len() > 0 {
|
||||
args = append(args, currentArg.String())
|
||||
currentArg.Reset()
|
||||
}
|
||||
default:
|
||||
currentArg.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
if escaped || inQuotes {
|
||||
return nil, fmt.Errorf("unexpected end of string")
|
||||
}
|
||||
|
||||
if currentArg.Len() > 0 {
|
||||
args = append(args, currentArg.String())
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
|
|
|||
102
utils/utils_test.go
Normal file
102
utils/utils_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseCommandLine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
input string
|
||||
expectedSuccess []string
|
||||
expectedError string
|
||||
}
|
||||
|
||||
// When called by the API, the first argument of the command input is actually the command name
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
expectedSuccess: []string{},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "single argument",
|
||||
input: "arg1",
|
||||
expectedSuccess: []string{"arg1"},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "multiple arguments",
|
||||
input: "arg1 arg2 arg3",
|
||||
expectedSuccess: []string{"arg1", "arg2", "arg3"},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "multiple arguments with extra whitespace",
|
||||
input: " arg1\targ2 arg3 ",
|
||||
expectedSuccess: []string{"arg1", "arg2", "arg3"},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "multiple arguments with quotes and escaping",
|
||||
input: `"arg 1" arg2 "arg\"3"`,
|
||||
expectedSuccess: []string{"arg 1", "arg2", `arg"3`},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "unquoted escaped whitespace",
|
||||
input: `arg\ 1 arg2`,
|
||||
expectedSuccess: []string{"arg 1", "arg2"},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "escaped JSON",
|
||||
input: `{\"hello\":\"world\"}`,
|
||||
expectedSuccess: []string{`{"hello":"world"}`},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "escaped JSON with space",
|
||||
input: `"{\"hello\": \"world\"}"`,
|
||||
expectedSuccess: []string{`{"hello": "world"}`},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "unclosed quote",
|
||||
input: `"arg 1", "arg2", "arg\"3`,
|
||||
expectedSuccess: nil,
|
||||
expectedError: "unexpected end of string",
|
||||
},
|
||||
{
|
||||
name: "three quotes",
|
||||
input: `"""`,
|
||||
expectedSuccess: nil,
|
||||
expectedError: "unexpected end of string",
|
||||
},
|
||||
{
|
||||
name: "unfinished escape",
|
||||
input: `arg\`,
|
||||
expectedSuccess: nil,
|
||||
expectedError: "unexpected end of string",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
parsedArgs, err := ParseCommandLine(tc.input)
|
||||
if tc.expectedError == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedSuccess, parsedArgs)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedError)
|
||||
assert.Empty(t, parsedArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -921,6 +921,38 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: *nodeHealth, Error: ""}
|
||||
case "/api/commands":
|
||||
nodeCommandsResponse, err := app.api.GetCustomNodeCommands()
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
"method": method,
|
||||
"body": body,
|
||||
}).WithError(err).Error("Failed to get node commands")
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: nodeCommandsResponse, Error: ""}
|
||||
case "/api/command":
|
||||
commandRequest := &api.ExecuteCustomNodeCommandRequest{}
|
||||
err := json.Unmarshal([]byte(body), commandRequest)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
"method": method,
|
||||
"body": body,
|
||||
}).WithError(err).Error("Failed to decode request to wails router")
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
commandResponse, err := app.api.ExecuteCustomNodeCommand(ctx, commandRequest.Command)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
"method": method,
|
||||
"body": body,
|
||||
}).WithError(err).Error("Failed to execute command")
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: commandResponse, Error: ""}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(route, "/api/log/") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue